Privacy-preserving peer-to-peer lending on Ethereum, powered by Zama FHEVM.
Privance enables borrowers and lenders to transact without exposing credit scores, loan amounts, or matching criteria on-chain. All sensitive comparisons are computed in the encrypted domain using Fully Homomorphic Encryption (FHE) — the result of a match check is an encrypted boolean that only the intended parties can decrypt.
Zama Developer Program — Mainnet Season 2, Builder Track submission.
On-chain lending is either:
- Transparent — everyone sees your credit score, loan amount, and counterparty criteria; or
- Off-chain — you trust a centralized intermediary.
Privance solves this with on-chain FHE computation: scores and thresholds stay encrypted in storage and are compared without ever being decrypted on-chain.
Borrower Lender
│ │
├─ computeCreditScore() ├─ createLenderOffer(minScore, maxAmount)
│ (FHE: Tier1 + Tier2 → euint64) │ (deposits ETH, params encrypted)
│ │
├─ createLoanRequest(amount, duration) │
│ (encrypted amount, plain amount) │
│ │
├──────── checkLoanMatch(loanId, offerId) ──┤
│ (FHE: score >= min AND │
│ amount <= max → ebool) │
│ │
│ [lender decrypts ebool off-chain] │
│ │
└─ fundLoan(loanId, offerId) ──────────────►│
ETH sent to borrower │
collateral locked │
RepaymentAgreement created │
│ │
├─ makePayment(agreementId) ───────────────►│
│ (ETH forwarded to lender) │
│ (collateral released on full repayment) │
│ │
└─ [or checkDefault() by anyone after due] │
collateral liquidated to lender
Three contracts form the Privance v2 protocol, all deployed and wired by the deploy scripts.
The central coordinator. Owns the FHE credit scoring logic and orchestrates loan lifecycle.
| Function | Description |
|---|---|
computeCreditScore() |
Computes an encrypted FICO-analogous score (300–850) from on-chain data |
createLoanRequest(...) |
Borrower posts an encrypted loan request; requires a valid score |
cancelLoanRequest(loanId) |
Borrower withdraws an unfunded request |
createLenderOffer(...) |
Lender deposits ETH with encrypted criteria |
cancelLenderOffer(offerId) |
Lender withdraws offer and reclaims ETH |
checkLoanMatch(loanId, offerId) |
FHE comparison → encrypted ebool stored on-chain |
fundLoan(loanId, offerId) |
Lender funds; transfers exact plainRequestedAmount to borrower |
setAavePool(address) |
Owner sets Aave V3 Pool for Tier-2 scoring (optional) |
setScoreValidityPeriod(seconds) |
Owner sets how long a score stays valid; 0 = never expires |
Manages ETH collateral. Both LendingMarketplace (for locking) and RepaymentTracker (for release/liquidation) are authorized callers.
| Function | Caller | Description |
|---|---|---|
depositCollateral() |
Anyone | Deposit ETH collateral |
withdrawCollateral(amount) |
Borrower | Withdraw unlocked collateral |
lockCollateral(borrower, amount, loanId) |
Marketplace only | Lock collateral at loan funding |
releaseCollateral(loanId) |
Marketplace or RepaymentTracker | Release after repayment |
liquidateCollateral(loanId, liquidator) |
Marketplace or RepaymentTracker | Seize to lender on default |
updateRepaymentTracker(address) |
Owner | Wire RepaymentTracker authorization |
Manages repayment agreements, payment forwarding, and default enforcement.
| Function | Description |
|---|---|
createAgreement(...) |
Called by Marketplace at funding; creates a RepaymentAgreement |
makePayment(agreementId) |
Borrower sends ETH; forwarded directly to lender |
checkDefault(agreementId) |
Anyone calls after due date; liquidates collateral if overdue |
getAgreementStatus(id) |
Returns "ACTIVE" / "REPAID" / "DEFAULTED" / "OVERDUE" |
Scores are clamped to 300–850 (FICO-analogous). They are stored as euint64 ciphertext; only the borrower can decrypt their own score via the Zama Relayer.
score = clamp(base + repaymentBonus - penalty + aaveBonus, 300, 850)
| Component | Details |
|---|---|
| Base | 500 |
| Repayment bonus (Tier 1) | +50 per completed Privance loan, capped at +300 |
| Default penalty (Tier 1) | −100 per defaulted loan, capped to prevent underflow |
| Aave health factor bonus (Tier 2) | 0–200 pts, mapped from Aave V3 health factor (optional) |
Reads the borrower's own RepaymentTracker agreements. Fully trustless — all data lives in the same protocol.
Reads the borrower's public healthFactor from Aave V3 Pool via IAaveV3Pool.getUserAccountData. The health factor is a plain uint256 read from a public view, so it can be safely converted to an encrypted value. This is zero-trust — no oracle, no off-chain feed.
| Health Factor | Aave Bonus |
|---|---|
| ≥ 3.0× | +200 |
| ≥ 2.0× | +150 |
| ≥ 1.5× | +100 |
| ≥ 1.0× | +50 |
| < 1.0× | 0 |
Tier 2 is opt-in — set setAavePool(address(0)) to disable it. If the Aave call reverts for any reason, the bonus falls back to 0 gracefully.
Scores can be configured to expire via setScoreValidityPeriod(seconds). Setting 0 disables expiry. Expired scores block createLoanRequest — borrowers must recompute before posting a new request.
Privance/
├── packages/
│ ├── hardhat/ # Contracts, tests, deploy scripts
│ │ ├── contracts/
│ │ │ ├── LendingMarketplace.sol
│ │ │ ├── CollateralManager.sol
│ │ │ ├── RepaymentTracker.sol
│ │ │ └── IAaveV3Pool.sol # Minimal Aave V3 Pool interface
│ │ ├── deploy/
│ │ │ ├── 01_deploy_collateral.ts
│ │ │ ├── 02_deploy_repayment.ts
│ │ │ └── 03_deploy_marketplace.ts
│ │ └── test/
│ │ └── LendingMarketplace.test.ts
│ ├── fhevm-sdk/ # Zama FHEVM SDK wrapper
│ └── nextjs/ # Frontend (React + Next.js)
└── README.md
- Node.js v18+
- pnpm v8+
- A funded wallet (deployer) with
MNEMONICset
pnpm installSet Hardhat configuration variables as documented in the Zama setup guide:
cd packages/hardhat
npx hardhat vars set MNEMONIC
npx hardhat vars set INFURA_API_KEY # for Sepoliacd packages/hardhat
npx hardhat compileTypeChain types are regenerated automatically into packages/hardhat/types/.
All 54 tests pass against the Hardhat local network with the FHEVM mock coprocessor.
Latest run result (Apr 17, 2026): 54 passing (6s).
cd packages/hardhat
npx hardhat test --network hardhat| Suite | Tests |
|---|---|
| Deployment | 3 — contract wiring, addresses, ownership |
computeCreditScore |
5 — base score, re-computation, expiry |
createLoanRequest |
6 — lifecycle, cancellation, revert cases |
createLenderOffer |
4 — lifecycle, cancellation, revert cases |
checkLoanMatch |
6 — match storage, idempotency, revert cases |
fundLoan |
10 — transfers, collateral locking, agreement creation, reverts |
makePayment |
7 — partial/full repayment, score update, reverts |
checkDefault |
5 — before/after due date, liquidation, score penalty |
CollateralManager |
4 — deposit, withdraw, locked balance |
| Admin controls | 3 — setAavePool, setScoreValidityPeriod ACL |
Contracts are deployed in order using hardhat-deploy. Script 03_deploy_marketplace.ts wires all cross-contract references automatically.
# Terminal 1
cd packages/hardhat
npx hardhat node
# Terminal 2
npx hardhat deploy --network localhostcd packages/hardhat
npx hardhat deploy --network sepoliaTo enable Tier-2 scoring on Sepolia:
# Aave V3 Pool on Sepolia: 0x6Ae43d3271ff6888e7Fc43Fd7321a503ff738951
npx hardhat run --network sepolia scripts/set-aave-pool.tsOr call setAavePool(poolAddress) directly from the owner wallet.
03_deploy_marketplace.ts performs these steps automatically:
- Deploy
LendingMarketplace(collateralAddress, repaymentAddress) CollateralManager.updateMarketplace(marketplaceAddress)RepaymentTracker.updateMarketplace(marketplaceAddress)CollateralManager.updateRepaymentTracker(repaymentAddress)← v2 fix
Step 4 is the critical v2 addition: it authorizes RepaymentTracker to call releaseCollateral and liquidateCollateral on CollateralManager, which was blocked in v1.
- No oracle dependency — all scoring inputs are read from public on-chain state (Privance protocol state + Aave public view).
- Underflow protection — default penalty is capped to total gains before subtraction in the FHE domain.
- Collateral check at funding —
fundLoanverifies available collateral before locking, preventing dust attacks. - Exact transfer —
fundLoantransfers exactlyplainRequestedAmount, not the offer's fullavailableFunds. - Offer stays live — a lender offer remains active until funds are exhausted; multiple loans can be funded from one offer.
- Access control —
lockCollateralis restricted toLendingMarketplace;releaseCollateral/liquidateCollateralare restricted toLendingMarketplaceorRepaymentTracker.
| Operation | Purpose |
|---|---|
FHE.asEuint64 |
Wrap plaintext scores into ciphertext |
FHE.fromExternal |
Decrypt user-submitted encrypted inputs (with ZKP proof) |
FHE.add / FHE.sub / FHE.mul |
Score arithmetic |
FHE.gt / FHE.lt / FHE.ge / FHE.le |
Score comparisons |
FHE.and |
Combine score match AND amount match |
FHE.select |
Encrypted conditional (used for clamping and capping) |
FHE.allowThis |
Grant the contract access to its own ciphertext handles |
FHE.allow |
Grant a specific address (borrower/lender) decryption rights |
Collateral amounts in CollateralManager.sol are stored as plaintext uint256.
Encrypting these fields would require rewriting getTotalLockedCollateral(),
which iterates over a dynamic array and sums values — an operation that is
prohibitively expensive with FHE without a full architectural redesign using
Gateway round-trips.
Proposed future path: Replace the summation loop with an encrypted running
total updated incrementally on lock/release events, using euint64 with
FHE.add() and a single Gateway decryption for availability checks.
Principal, interest rate, and repayment amounts remain plaintext to support
on-chain arithmetic in makePayment(). Access to these values is restricted
to borrower, lender, and the marketplace contract via getAgreementDetails().
- Zama FHEVM Documentation
- FHEVM Solidity Library
- Relayer SDK
- Aave V3 Developer Docs
- Zama Developer Program
BSD-3-Clause-Clear. See LICENSE.