Skip to content

bleu/confidential-buybacks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ConfidentialBuybacks

Dark-pool protocol buybacks on Zama FHEVM. Built for the Zama Developer Program Builder Track. PoC/MVP quality — not audited, not production.

Live demo: (Vercel URL here) · Network: Sepolia only

ConfidentialBuybacks — treasury view with encrypted epoch state

The problem

Protocols doing on-chain buybacks telegraph their orders. The budget, timing, and every fill are public the moment the program starts — so MEV bots front-run the buys, and the market times its exits against the treasury. The treasury systematically overpays for its own token.

The idea

A standing dark pool where the treasury buys its own token directly from holders. The buyback budget, individual offers, fills, and each seller's price floor are FHE-encrypted on-chain — nobody (including other sellers) can see how much is being bought or sold, or at what limits. Orders rest in rolling settlement windows that anyone can settle at the oracle price once they expire; unspent budget carries over, and the treasury can top it up confidentially at any time — so an open window carries no information about actual demand. After a window settles and a disclosure delay passes, anyone can trigger public decryption of that window's total: confidential during execution, accountable afterward.

How it works

 Treasury                          BuybackVault                        Sellers
    │                                   │                                 │
    │ 1. fund vault with cUSDT          │                                 │
    │──────────────────────────────────▶│                                 │
    │ 2. openEpoch(enc(budget))         │                                 │
    │    topUp(enc(amount)) any time    │   3. setOperator(vault, 24h)    │
    │──────────────────────────────────▶│◀────────────────────────────────│
    │                                   │   4. submitOffer(enc(amount),   │
    │                                   │                  enc(minPrice)) │
    │                                   │◀────────────────────────────────│
    │                                   │   escrow cTOKEN, compute under FHE:
    │                                   │   fill      = min(offer, remaining)
    │                                   │   remaining = remaining − fill
    │                                   │   total    += fill              │
    │                                   │                                 │
    │  5. rollEpoch() — anyone, once the window expires (owner: any time) │
    │     snapshots oracle price, carries remaining budget to next window │
    │                                   │                                 │
    │                                   │   6. claim(window)              │
    │                                   │◀────────────────────────────────│
    │                                   │   under FHE: fill counts only if
    │                                   │   settlementPrice ≥ enc(minPrice)
    │                                   │   pay enc(fill × price) cUSDT   │
    │                                   │   refund the rest in cTOKEN     │
    │                                   │                                 │
    │        7. after 5 min: requestDisclosure + finalizeDisclosure       │
    │           (anyone) → plaintext window total, KMS-verified           │

The FHE.min running-budget pattern

Fills are first-come-first-served against an encrypted running budget — the core trick that keeps every offer O(1) FHE operations, with no loops, no sorting, and no encrypted division:

// matching, at offer time:
euint64 fill = FHE.min(offer, remaining);       // clamp to what's left (encrypted)
remaining    = FHE.sub(remaining, fill);        // can't underflow: fill ≤ remaining
totalFilled  = FHE.add(totalFilled, fill);

// settlement, at claim time (settlementPrice is the public oracle snapshot):
ebool   floorMet      = FHE.le(minPrice, settlementPrice);
euint64 effectiveFill = FHE.select(floorMet, fill, zero);
euint64 payout        = FHE.div(FHE.mul(effectiveFill, settlementPrice), 100);

A seller whose offer exceeds the remaining budget is partially filled; once the budget is exhausted, later offers get zero fill — but no one can tell which, because offers, fills, and the budget are all ciphertexts. The same applies to price floors: a floor that misses the settlement price turns into a full refund, indistinguishably. Failed conditions become no-ops instead of reverts (never branch on encrypted values).

The escrow uses the actual transferred amount returned by ERC-7984's confidentialTransferFrom, so an offer backed by insufficient balance escrows 0 and fills 0 — you can't inflate the total with tokens you don't have.

Privacy model

Hidden (encrypted) Public
Buyback budget, top-ups, carryover Oracle price & per-window settlement price
Remaining budget That an address submitted an offer (tx metadata)
Individual offer amounts Number of offers, window open/settle timing
Individual price floors (and whether they were met) Disclosed window totals (after delay, by design)
Individual fills & payouts Contract addresses, operator approvals
Cumulative bought (until disclosure)

Honest caveats: participation metadata is visible — observers can see who interacted with the vault and when; only amounts and floors are hidden. Other limitations: fills are first-come-first-served (no pro-rata), one offer per seller per window, no offer cancellation, budget reserved by a fill whose floor later fails is not recycled within that window, disclosed totals are a snapshot at disclosure time, and treasury solvency is not verified on-chain (if the vault is underfunded, claims transfer 0 cUSDT rather than reverting — ERC-7984 transfers are all-or-nothing and never revert on insufficient balance). The price oracle is an owner-set mock behind an interface a real feed adapter would implement.

Contracts (Sepolia)

Contract Address
ConfidentialGovToken (cTOKEN) 0xa2E95Db3Bb2f2B02b2990c66A74534D79684D80f
ConfidentialUSDT mock (cUSDT) 0x5ffb152C8D371Ae59c25689c9F0F6e8a914CcbcA
MockPriceOracle 0x9521848F454961dee8B42d51f0269Bd1F56B84F8
BuybackVault 0x27289cA07948178fbA7e08b3a7EBe868889621f2

Both tokens are ERC-7984 confidential tokens (euint64 amounts, 6 decimals) with an open capped faucet() for the demo. The payment token is a self-deployed mock (the official Sepolia cUSDT wrapper requires wrapping an underlying ERC-20; the vault takes the token address as a constructor param, so it can be swapped).

Prices are 2-decimal fixed point (210 = 2.10 cUSDT per cTOKEN): payout = fill × price / 100 uses only scalar FHE mul/div. Budgets are FHE-clamped to 1e15 and the settlement price capped at 10,000 (100.00), so the payout can never overflow euint64. Settlement windows default to 15 minutes on the demo deployment.

Repository layout

confidential-buybacks/
├── contracts/        # Hardhat project (Zama FHEVM template)
│   ├── contracts/    # ConfidentialGovToken.sol, BuybackVault.sol, MockPriceOracle.sol
│   ├── test/         # 21 tests on the FHEVM mock, incl. full disclosure proof flow
│   ├── deploy/       # hardhat-deploy script
│   └── scripts/      # seed.ts (fund vault + open epoch), verify-state.ts
└── frontend/         # Next.js app (wagmi + viem + Zama relayer SDK)

Setup

Requires Node ≥ 20.

# Contracts
cd contracts
npm install
npm test                                  # 21 tests on the FHEVM mock

# Deploy to Sepolia (.env: PRIVATE_KEY, RPC_URL; optional CUSDT_ADDRESS)
npx hardhat deploy --network sepolia
npx hardhat run scripts/seed.ts --network sepolia    # fund vault + open demo epoch

# Frontend (contract addresses live in src/config/contracts.ts)
cd ../frontend
npm install
npm run dev

Demo flow with two wallets: Seller — faucet cTOKEN → approve vault as operator (24 h) → submit encrypted offer with a private price floor. Treasury — decrypt remaining/total, move the mock oracle, settle the window. Seller — decrypt fill + floor, claim payout + refund. Anyone — after 5 minutes, request + publish the window total on the transparency tab.

Stack

@fhevm/solidity 0.11 · @openzeppelin/confidential-contracts 0.5 (ERC-7984) · @fhevm/hardhat-plugin mock testing · @zama-fhe/relayer-sdk 0.4 (client-side encryption, userDecrypt with EIP-712, publicDecrypt + on-chain FHE.checkSignatures) · Next.js 15, wagmi/viem.

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors