Inspiration
Traditional DeFi lending is stuck in an overcollateralization trap — protocols like Aave and Compound require 150%+ collateral, excluding billions of creditworthy people who don't have enough capital to lock up. Real-world credit systems work differently: banks lend based on who you are, not just what you can put up as collateral.
The problem? Bringing credit scores on-chain feels impossible without sacrificing privacy. If you publish your credit score to a blockchain, you've exposed sensitive financial data to the entire world — permanently.
Zero-Knowledge proofs change this equation entirely. I asked: what if you could prove you're creditworthy without revealing anything about yourself?
What it does
ZKredit is a DeFi lending protocol that enables undercollateralized loans by verifying creditworthiness using Zero-Knowledge proofs — without ever exposing the user's actual credit score or income on-chain.
Key features:
- 50% collateral ratio instead of the standard 150% — enabled by ZK-verified credit checks
- Privacy-first borrowing — your credit score and income never leave your browser
- In-browser ZK proof generation — powered by Barretenberg WASM (Aztec's proving system)
- On-chain proof verification — the
UltraVerifiercontract verifies the proof before releasing funds - Full DeFi primitives — deposit, borrow, repay, and liquidate functions with reentrancy protection
How it works:
- User enters their credit score (≥700) and income (≥$50,000) locally
- A Noir circuit asserts both conditions hold using ZK assertions:
assert(credit_score >= threshold_score); assert(income >= threshold_income); - Barretenberg generates a proof entirely in the browser — private inputs never leave the device
- Only the proof bytes are submitted on-chain to
LendingPool.borrow() - The deployed
UltraVerifier.solcontract verifies the proof, and if valid, transfers tokens at 50% collateral
The blockchain sees only: "this user proved they meet the credit requirements" — nothing more.
How I built it
The project is structured across three layers:
Layer 1 — ZK Circuit (Noir)
The core circuit (circuits/src/main.nr) is written in Noir, Aztec's ZK-focused DSL. It takes private inputs (credit_score, income) and public inputs (threshold_score, threshold_income) and asserts both conditions. Running nargo compile + bb write_vk generates the UltraVerifier.sol Solidity contract containing the hardcoded verification key.
Layer 2 — Smart Contracts (Solidity + Foundry)
LendingPool.sol implements the four core DeFi functions. The borrow() function calls IUltraVerifier.verify(proof, publicInputs) before checking the 50% collateral ratio. Strict conventions were applied throughout — i_ prefix for immutables, s_ for storage, NatSpec documentation, and OpenZeppelin's ReentrancyGuard + SafeERC20.
Testing was done at three levels using Foundry:
- Unit tests — deposit, borrow with valid proof, collateral enforcement, repay
- Fuzz tests — 256 runs each testing random amounts, multi-user independence, edge cases
- Invariant tests — 256 sequences × 500 calls = 128,000 actions per invariant, verifying solvency (
deposits ≥ borrows), accounting integrity, and collateral enforcement
Final result: 23/23 tests passing, 100% line/branch/function coverage on LendingPool.sol.
Layer 3 — Frontend (Next.js + wagmi)
A Next.js 15 app where ZK proof generation runs entirely in the browser via @noir-lang/noir_js + @noir-lang/backend_barretenberg WASM. The UI has three tabs (Deposit, Borrow, Repay) and handles the full flow from wallet connection to proof submission.
Note: I am back-end developer with core skills in ZK and Smart Contracts. I used help of Claude to make a minimal front-end design to showcase the ZK Circuit implementation.
Challenges I ran into
1. In-browser ZK proving performance Barretenberg WASM is heavy — proof generation takes 10–30 seconds in the browser. We worked around this by providing clear UX feedback during generation. Web Workers would improve this further (listed in future work).
2. eth_estimateGas failures with MetaMask
MetaMask's gas estimation calls the contract before approval is set, causing reverts. The fix was to hardcode explicit gas limits on every writeContract call — 600,000 gas for borrow() (ZK verification is expensive), 120,000 for deposit/repay.
3. Barretenberg version compatibility
The Noir circuit's proving system (UltraHonk) and the bb CLI version must exactly match the @noir-lang/backend_barretenberg npm package version. A single minor version mismatch causes silent proof failures.
4. Real UltraVerifier deployment
The real UltraVerifier.sol (generated by Barretenberg) is a ~2,000-line contract with complex elliptic curve operations. Getting the full pipeline — nargo compile → bb write_vk → bb write_solidity_verifier — working end-to-end required careful orchestration via scripts/generate_verifier.sh.
5. Public inputs encoding
Solidity's bytes32[] public inputs array must match exactly what the Noir circuit expects. The threshold values (700, 50000) must be ABI-encoded as bytes32 in the correct order, matching the circuit's public input layout.
Accomplishments that I'm proud of
- First full-stack ZK DeFi protocol with end-to-end proof flow: circuit → verifier contract → frontend
- 100% test coverage on the lending contract with 128,000 invariant-tested state transitions
- In-browser ZK proving — the credit score never touches any server or blockchain; it stays on the user's device
- Real deployed UltraVerifier.sol — we actually generated the verification key from a compiled Noir circuit and embedded it in a Solidity contract
- Seamless UX despite the complexity of ZK proof generation happening under the hood
- The architecture demonstrates a genuinely new DeFi primitive: privacy-preserving undercollateralized lending
What I learned
- Noir as a ZK DSL is remarkably approachable — writing the circuit felt closer to Rust than to traditional ZK systems
- UltraHonk (Barretenberg's proving system) is genuinely practical for browser-side proving
- Foundry invariant testing is extraordinarily powerful — 128k state transitions surfaced no edge cases, which gave genuine confidence in the protocol's correctness
- The gap between "ZK proof works locally" and "ZK proof works in a browser with a wallet" is large — WASM thread isolation, COOP/COEP headers, and gas estimation all create friction
- On-chain ZK verification is expensive but feasible — 600k gas for
borrow()is high but acceptable for a credit-gated operation
What's next for ZKredit
- Deploy the real
UltraVerifier— replaceMockVerifierForDeploywith the Barretenberg-generated contract - Interest rate model — flat collateral ratio is a demo; production needs a dynamic rate curve
- Chainlink price feeds — collateral valuation should use oracle prices, not a fixed 1:1 ratio
- Actual credit data integration — connect to off-chain credit attestation systems (e.g., zkTLS proofs of credit bureau data)
- Web Workers for faster browser-side proof generation
- Liquidation UI — the contract supports liquidation; the frontend doesn't yet
- Governance — on-chain voting to update credit thresholds via a Governor + Timelock pattern
- Mainnet deployment after a formal security audit
Built With
- barretenberg
- bash
- blockchain
- cryptography
- ethereum
- etherscan
- finance
- fintech
- foundry
- next.js
- noir
- openzeppelin
- rainbow-kit
- react
- sepolia
- smart-contracts
- solidity
- typescript
- ultrahonk
- viem
- wagmi
- wallet-connect
- wasm
- web3
- zero-knowledge
- zk
Log in or sign up for Devpost to join the conversation.