Cirlo
Web3 Smart Contract · Crypto Lending
Cirlo is a lending platform that puts the trust boundary in the contract layer. Lenders lock liquidity into a vault, borrowers build an on-chain reputation score, and a loan manager runs the request-to-repayment lifecycle. The off-chain side is deliberately conventional — a Django API for users, loan records and history — so the interesting logic lives in Solidity where it can be read, tested and reasoned about directly.
Technology
The problem
Decentralised lending is a good exercise in trust boundaries, because the interesting problems are all about who is allowed to move which asset, and when. A lender must be able to lock liquidity without a third party taking it. A borrower must be able to build reputation without a third party deciding it for them. And the loan lifecycle must not allow the same funds to be spent twice or a reputation score to be edited while it is collateral.
Approach
- 01
Split the system into four contracts with one job each, rather than one contract doing everything: a vault that holds funds, a pool that tracks deposits and routes them, a reputation manager that owns the score, and a loan manager that runs the request → approve → repay or default lifecycle.
- 02
Applied the standard guard rails to every fund-moving function: reentrancy protection, pausability for emergency stops, safe token transfer helpers, and role-based access so only the intended contract or owner can call each entry point.
- 03
Made double-spending structurally impossible rather than checking for it — funds allocated to an active loan cannot be withdrawn, and reputation is locked while a loan is outstanding, so a borrower cannot improve their score mid-loan to escape the obligation.
- 04
Kept the off-chain side ordinary. A Django API with JWT authentication, OAuth and wallet-signature login handles users, loan records and transaction indexing, and reads chain state through Web3.py rather than duplicating it.
- 05
Indexed events into PostgreSQL with scheduled Celery tasks — continuous event scanning, hourly overdue-loan checks, and periodic reputation snapshots — so history and reporting are queries rather than a slow chain walk on every page load.
- 06
Ran the whole stack under Docker Compose, and the contracts against a local Hardhat network with a written test suite, because a lending contract that has only been manually clicked through has not been verified.
Architecture
- 01
Next.js frontend
Wallet connection, an onboarding flow, and separate lender, borrower, reputation and history views over the same API.
- 02
Django API
JWT authentication with Google and wallet signature, loan lifecycle endpoints, and a least-privilege PostgreSQL store for users, loans, reputation history and transaction logs.
- 03
Chain event indexing
Scheduled Celery tasks scan for new events and project them into relational history, so the interface never has to walk the chain itself.
- 04
Smart contracts (Base)
LiquidityVault for custody, LendingPool for deposits and routing, ReputationManager for trust scores, LoanManager for the loan lifecycle — all guarded, pausable and role-restricted.
Four contracts, one responsibility each, with reentrancy guards, pausable emergency stops and role-based access on every fund-moving path.
Key features
Challenges
Making the trust boundaries explicit in Solidity
Each contract was given a single responsibility and a narrow permission set, so 'who can move this money' has one answer per contract instead of a tangle of inherited allowances. The alternative is a system where the security of the whole thing depends on a modifier someone remembered to add.
Preventing reputation manipulation during a loan
A reputation score that stays editable while it is collateral is not collateral. Locking the score for the lifetime of an active loan removes an entire class of exploit by making the dishonest state unreachable rather than merely discouraged.
Making chain history queryable
Walking the chain on every page load is slow and gets slower. Indexing events into relational tables on a schedule gives the interface ordinary database queries, and gives overdue-loan detection a place to run hourly instead of per-request.
What I learned
On-chain code is unforgiving in a way application code is not — there is no emergency rollback, only an upgrade or a migration. That constraint is genuinely clarifying: it forces the invariants into the type system early, which is the same discipline you want in a payments backend, for the same reason.
Scope & current state
Being precise about scope
- This is an experimental engineering project. It is not production financial infrastructure, it has not been audited, and it has not been deployed with real value.
- Contracts are written in Solidity, tested against a local Hardhat network, and configured for Base testnet deployment. The roadmap in the repository is explicitly unfinished.