Borrow, leverage, and net your positions with the amounts, the debt, and even your leverage ratio encrypted end-to-end. No plaintext position ever touches the chain. Built on Zama FHEVM. Live on Sepolia.
- Why GhostLend
- Architecture
- The three pillars
- Under the hood — computing on ciphertext
- Confidential leverage
- Liquidation that reveals one bit
- GhostGate netting
- The hard parts — FHE-native engineering
- Deployed contracts (Sepolia)
- Markets
- Tech stack
- Local development
- Testing & coverage
- What's hidden vs public
- Limitations & roadmap
Every lending position you open onchain is a public broadcast. Your size, your debt, your liquidation price, your leverage — all readable by anyone with an RPC endpoint. We treat that as normal; it isn't. It's an adversarial disadvantage baked into the rails, and tens of billions in lending/vesting TVL sit exposed line by line.
The supply side of confidential DeFi already exists — Zama's Protocol brings FHE to Ethereum, and the confidential vault from Steakhouse × Morpho lets you shield deposits and earn encrypted yield ($18M+ TVS live on mainnet). But there was a hole: you could shield your yield… then had to go completely naked to borrow against it.
GhostLend is the missing half — the credit layer. Same protocol, same open-source ERC-7984 primitives. We add borrow, leverage, and netting — all confidential.
graph TD
E["Ethereum"] --> Z["Zama Protocol — FHE on Ethereum"]
Z --> V["Confidential Vault · Steakhouse × Morpho<br/>shield deposits · earn encrypted yield · $18M+ TVS"]
Z --> G["GhostLend · the CREDIT layer<br/>borrow · leverage · GhostGate netting"]
V -. "confidential collateral" .-> G
classDef ours fill:#ffd208,stroke:#1a1a1a,color:#1a1a1a,font-weight:bold;
classDef base fill:#16181f,stroke:#2f333d,color:#eceef2;
class G ours;
class E,Z,V base;
graph LR
User(["👤 User"]) -->|"confidential ops (euint64)"| Pool["GhostLendPool<br/>M0 · M1 · M2"]
Pool --> Oracle["OracleAdapter<br/>Chainlink ETH/USD"]
Pool --> Vault["MockYieldVault<br/>share price oracle"]
User -->|"deposit / withdraw intents"| Gate["GhostGate<br/>netting gateway"]
Gate --> DB["DepositBatcher"]
Gate --> WB["WithdrawBatcher"]
Gate -->|"only NET crosses"| Vault
Keeper(["🤖 Keeper"]) -. "epoch · liquidation · dispatch" .-> Pool
Keeper -.-> Gate
graph TB
subgraph INSTANT["⚡ Native lending — INSTANT (no bridge)"]
direction LR
A1["👤 You"] -->|"1 atomic tx"| P1["GhostLend Pool"]
P1 -->|"encrypted position"| A1
end
subgraph BATCHED["🕒 Vault boundary — BATCHED (GhostGate)"]
direction LR
A2["👤 You"] --> N1["Net inside<br/>encrypted zone"]
N1 -->|"only NET flow crosses"| PV["Public Vault"]
end
Native lending needs no bridge, so it's instant. We batch only where confidential value crosses to public — the same design as Zama's own vault (60s window on testnet, ~12h on mainnet).
Rates lag one epoch by design: utilization is only ever revealed as a public aggregate, once per epoch — individual positions never are.
graph LR
C["closeEpoch<br/>snapshot encrypted aggregates"] --> M["makePubliclyDecryptable"]
M --> D["KMS publicDecrypt<br/>(off-chain, ~3s)"]
D --> F["finalizeEpoch<br/>checkSignatures → set borrow rate"]
F -. "next epoch · 300s gate" .-> C
| Pillar | What it does | Why it's special |
|---|---|---|
| 1 · Confidential lending | Isolated markets (cWETH ↔ cUSDC) with encrypted supplied / collateral / debt | Instant, atomic, end-to-end encrypted — no batches |
| 2 · Vault-collateralized leverage | Loop your Earn position up to 4× in one tx against csteakcUSDC |
The leverage ratio itself is encrypted (euint8); swap-free |
| 3 · Zero-leak cross-asset borrow | Deposit confidential dollars → borrow confidential ETH via Market 1 composition | No DEX, no public swap leg — every step an encrypted pool op |
This is what makes GhostLend more than a wrapper. Your balances are euint64 — encrypted 64-bit integers in contract storage. When you borrow, the pool never decrypts. It computes on the encrypted state:
- Adds your encrypted collateral and clamps the borrow to an encrypted credit limit — no amount revealed.
- Checks health with an encrypted comparison (
FHE.lt) returning anebool, not a number. - Applies the outcome with branchless selection (
FHE.select) — never revealing which branch was taken.
Transactions never revert on your balance. A failed borrow is silently clamped to your encrypted maximum, so even the revert reason can't be used to binary-search your position. Leakage is treated as the enemy — down to the error path.
sequenceDiagram
autonumber
participant U as 👤 You
participant P as GhostLendPool
participant T as Pool Treasury (cSHARE)
U->>P: openLeveragedYield(deposit, lev = euint8)
Note over P: target = lev · deposit<br/>FHE.select chain over {1,2,3,4}<br/>(no encrypted multiply)
P->>T: draw leveraged collateral (1 tx, no re-approvals)
Note over P: record debt (euint64)<br/>born healthy — 66.7% < 90% LLTV
P-->>U: encrypted position — ratio NEVER revealed
A real, on-chain worked example:
| Metric | Value |
|---|---|
| Position from 10k cUSDC @ 3× | 30,000 |
| Debt / collateral (born healthy vs 90% LLTV) | 66.7% |
Net carry = 8% vault × 3 − 3.15% borrow × 2 |
+17.7% |
Because Market 2 is same-asset (a confidential dollar vault-share against confidential dollars) there's no swap and no public leg — the loop never touches a DEX. Same-asset ⇒ no cross-asset price risk ⇒ the aggressive 90% LLTV.
On a transparent lender, everyone sees your leverage and hunts your liquidation. Here, the position, the debt, and the ratio are all ciphertext.
sequenceDiagram
autonumber
participant K as 🤖 Keeper (permissionless)
participant P as GhostLendPool
participant KMS as 🔐 Zama KMS
K->>P: poke(market, user)
Note over P: unhealthy = FHE.lt(creditLimit, debtActual)<br/>→ ebool (still encrypted)
P->>KMS: makePubliclyDecryptable(unhealthy)
KMS-->>K: publicDecrypt → 1 BIT + proof
K->>P: finalizeLiquidation(bit, proof) + checkSignatures
Note over P: re-check CURRENT health<br/>FHE.select(stillUnhealthy, seize, 0)
Note over P: position size NEVER leaks — only the health bit
Health is evaluated on ciphertext; the only value ever decrypted is a single healthy/unhealthy boolean, threshold-decrypted by the KMS and verified onchain. A borrower who cured between poke and finalize is seized zero — branchlessly.
✅ Verified live on Sepolia. A real liquidation executed end-to-end — no faked health, no oracle manipulation:
poke→ KMS one-bit decrypt →finalizeLiquidation.nextPokeId0 → 1; the position's encrypted debt was wiped and collateral seized on-chain. Harness:scripts/liquidate.ts.
graph LR
D["Deposit intents<br/>(encrypted)"] --> NET{"Net inside the<br/>encrypted zone"}
W["Withdraw intents<br/>(encrypted)"] --> NET
NET -->|"reveal ONLY dir + net"| CROSS["Single net flow<br/>crosses to public vault"]
NET -. "gross D/W and every<br/>per-user intent stay<br/>encrypted forever" .-> HID["🚫 hidden"]
Deposits net against withdrawals inside the confidential zone; only two values ever cross — the net direction and net amount. Never who deposited, on which side, or how much. Illustratively, ~80% less on-chain-visible volume. This is Zama's own published v2 direction: internal netting toward a single confidential gateway.
The genuinely difficult part isn't wrapping a token — it's the pile of FHE constraints you have to design around.
| # | The constraint (FHE reality) | How GhostLend solves it |
|---|---|---|
| ① | No ciphertext ÷ ciphertext — you can only multiply by a plaintext scalar | Credit/rate math folded into plaintext coefficients (_mulDivScalar); GhostGate uses pin-only settlement — we refused to reveal one side's total (it would leak both aggregates) |
| ② | A null handle bytes32(0) bricks the KMS — it rejects it and freezes the epoch machine |
Constructor baselines every aggregate to a real, trivially-decryptable FHE.asEuint64(0) + an activity gate on closeEpoch. Found by self-audit, regression-tested. |
| ③ | You can't branch on a ciphertext — if (encrypted) doesn't exist |
Every conditional is FHE.select; leverage target is a select-chain over euint8 ∈ {1,2,3,4}; seize is gated on a re-derived health bit |
| ④ | Reveals are asynchronous (makePubliclyDecryptable → KMS publicDecrypt → finalize) |
Two-phase state machines for epochs & liquidation, with replay guards + self-healing recovery from event logs; finalize stays under budget (~2.78M HCU < 5M ceiling) |
| ⑤ | ACL hygiene is load-bearing — wrong ACL ⇒ next op reverts or KMS won't decrypt | Every stored handle allowThis + allow(user); outgoing transfers allowTransient; finalizers rebuild handles from storage under replay guards |
None of these are in the "wrap an ERC-20" starter. They're the difference between a demo and a protocol.
Network: Ethereum Sepolia (chainId
11155111) · All 7 core contracts Etherscan-verified ✅
| Contract | Address | Purpose |
|---|---|---|
| GhostLendPool | 0x1E7Bc1…860b7 |
3 isolated markets (M0/M1/M2) |
| OracleAdapter | 0x088362…c8495 |
Chainlink ETH/USD wrapper, 2h staleness guard |
| GhostGate | 0xb3D9A7…3cb95 |
Netting gateway (dir + net only) |
| Contract | Address | Purpose |
|---|---|---|
| MockYieldVault (gYVS) | 0xfaC681…3838 |
ERC-4626 share-price oracle |
| csteakcUSDC | 0x324A43…f8d8 |
Confidential vault-share wrapper (ERC-7984) |
| DepositBatcher | 0xc0C680…9FA43 |
cUSDC → vault → cSHARE |
| WithdrawBatcher | 0x97576E…1112 |
cSHARE → vault → cUSDC |
| Token | Wrapper | Underlying |
|---|---|---|
| cUSDC (6 dec) | 0x7c5BF4…3639 |
USDC 0x9b5Cd1…DFfF |
| cWETH (6 dec) | 0x462086…3158 |
WETH 0xff5473…5f3F |
| ID | Collateral → Debt | LLTV | Pricing |
|---|---|---|---|
| M0 | cWETH → cUSDC | 80% | Chainlink ETH/USD |
| M1 | cUSDC → cWETH | 80% | Chainlink ETH/USD |
| M2 | csteakcUSDC → cUSDC | 90% | Vault share price (swap-free leverage) |
| Layer | Technology |
|---|---|
| FHE | Zama FHEVM · @fhevm/solidity 0.11.1 · @zama-fhe/sdk + react-sdk 3.2.0 |
| Confidential tokens | OpenZeppelin confidential-contracts 0.5.1 (ERC-7984) |
| Oracle | Chainlink ETH/USD |
| Contracts | Solidity 0.8.27 · Hardhat · viaIR · optimizer 800 |
| Frontend | Next.js · wagmi / viem · @zama-fhe/react-sdk · deployed on Vercel |
| Keeper | Node/Hardhat bot — epoch finalize · liquidation · GhostGate dispatch |
# 1 · install
npm install
# 2 · run the mock-FHEVM test suite
npm test
# 3 · coverage
npx hardhat coverage
# 4 · deploy (needs an ETHERSCAN_API_KEY + funded deployer key in hardhat vars)
npx hardhat run scripts/deploy-all.ts --network sepolia
# 5 · run the keeper (epoch + liquidation + GhostGate, self-healing loop)
KEEPER_LOOP=1 npx hardhat run scripts/keeper.ts --network sepolia
# frontend
cd frontend && npm install && npm run dev- Mock-FHEVM suites green · 36 tests including targeted audit-regression tests
- 91% line coverage (97% on the core
GhostLendPool) - Liquidation finalize measured at ~2.78M HCU (under the 5M homomorphic-complexity ceiling)
- FHE ACL hygiene verified — every stored handle
allowThis+allow(user), outgoing transfersallowTransient, finalizers rebuild handles under replay guards +checkSignatures - Every audit finding (H-1 epoch-brick, H-2 liquidation over-seize, M-1 cash accounting) fixed, redeployed, and regression-tested
🕶 What's hidden vs public
🔒 Encrypted (euint64, per user) |
🌐 Public |
|---|---|
| Supplied principal, collateral, debt | Interest indexes, rates, utilization (per epoch) |
| Per-op error flag | Prices, LLTV, timestamps |
Leverage ratio (euint8) |
Participant addresses (ERC-7984 exposes from/to; amounts stay encrypted) |
| GhostGate gross D/W + per-user intents | Liquidation reveals one bit (healthy/unhealthy) |
We state the boundary instead of hiding it. Addresses and timing are public by Ethereum's nature; what matters — amounts, positions, leverage, health — is ciphertext. Aggregate utilization is revealed per epoch purely for solvency, exactly the trade-off Zama's own vault makes.
Honest by design — a Developer-Program checkpoint, not a finished mainnet protocol:
- Rates are epoch-lagged & keeper-dependent — utilization is the last revealed value; rates freeze if the keeper is down. Roadmap: keeperless/incentivised epoch closing.
- IRM quantization — the per-second rate is 1e9 fixed-point and truncates at low utilization (nominal 4.5% → ~3.15% effective borrow; supply rate rounds to 0 at these utils). Roadmap: higher-precision index.
- Vault yield is simulated —
MockYieldVaultis the only mock (honestly labeled in-contract & UI as a Steakhouse-Prime replica); yield is a keeper drip. Roadmap: real confidential ERC-4626 strategy. - Oracle — Chainlink ETH/USD only; USDC assumed = $1; vault share-price is a plaintext oracle.
- Batch anonymity is N−1 within a
minBatchAgewindow; participation/timing are public. - Single off-chain keeper with a hot key drives epoch/liquidation/gate — not yet decentralized.
Full design-of-record: docs/ARCHITECTURE.md · docs/ADDENDUM.md · docs/BATCHER-NOTES.md · deployments/ADDRESSES.md
🌐 ghostlend.vercel.app · 📊 Deck · 🔐 Zama Protocol
GhostLend is the native half of confidential DeFi — borrow, leverage, and net, with everything encrypted.