Cross-chain credit reputation and lending on Creditcoin, priced on proof rather than trust.
→ crosscredit.vercel.app — live on testnet, no wallet needed to look around
CrossCredit reads a borrower's real Aave V3 history from Ethereum mainnet and proves it to Creditcoin using the Attestcoin Protocol's native block-prover precompile — a Merkle inclusion proof plus a chain-continuity proof, verified synchronously inside a Creditcoin block. Verified history becomes an on-chain credit score, a soulbound Credit Tier NFT, and a real loan: Platinum borrows undercollateralized at 85%.
Nothing is self-reported and no oracle delivers a number. The score is computed inside the EVM of the chain that verified the evidence, from events that chain checked for itself.
0x76f30e3f…5b1A is a real Ethereum wallet with a real Aave V3 borrowing record. It has never sent
a transaction to Creditcoin. We proved five of its genuine mainnet repayments — one execute each,
no permission from the borrower needed or possible — and watched a credit profile assemble itself:
score 0 → 320 → 440 → 560 → 680 → 800 Bronze → Silver → Gold → Platinum
It can now borrow 1,000 tUSD against 850 of collateral on Creditcoin, on the strength of repayments it made on another chain to a protocol that has never heard of us either.
An earlier version of this project read only our own LoanBook on Sepolia. That contract is
permissionless, escrows nothing and takes a self-declared principal — there is no lender. So:
openLoan(1 wei) → repay{value: 1 wei}(loanId) × 6
Six wei, twelve transactions, one wallet, and the old model returned Platinum with 85% LTV. Every proof was cryptographically valid. The Merkle proof verified, the continuity proof verified, the precompile did its job perfectly. We had built a rigorous pipeline for the high-integrity delivery of worthless data.
The question that exposes it is one sentence: who was the lender?
You cannot fake an Aave loan. So the fix was to read the chain where the lenders are, and to make the arithmetic enforce the distinction:
The tier sets your rate.
demonstratedCapacityWei— the largest single amount you have provably repaid to a real third-party protocol — sets how much you may borrow above what you post. A Platinum wallet with no real history gets the Platinum rate on a fully collateralized loan and no discount at all.
Live on the same registry, right now:
| Borrower | History | Score | Tier | Capacity | Collateral for 1,000 tUSD |
|---|---|---|---|---|---|
0x76f30e…5b1A |
5 real Aave V3 repayments | 800 | Platinum | $960,145 | 850 |
0xe57D6C…dd00 |
1 real repayment, from Oct 2024 | 423 | Silver | $2 | 1,300 |
0xaA2871…55d5 |
9 self-dealt, one batch | 390 | Silver | $0 | 1,300 |
0x8ce707…89c6 |
9 self-dealt, proof by proof | 390 | Silver | $0 | 1,300 |
0xB82dC3…b52E |
2 self-dealt, one late | 0 | Bronze | $0 | 1,500 |
The second row is the time axis, and it is the one an attacker cannot script. That wallet reached 423 from a single proof — 120 for the repayment, 183 for capacity, and 120 for age, maxed — because the transaction it proved happened in October 2024. The Platinum wallet above it has five times the proofs and earns nothing from age, because everything it proved happened last week. Source-chain time is derived from the block height inside the Merkle proof, so it cannot be forged.
The bottom three are the control group. Their proofs are just as valid; their evidence is not.
Splitting a history across a thousand wallets divides capacity rather than multiplying it, so
sybil resistance here is arithmetic rather than an identity check —
test_capacityCap_isInvariantUnderIdentitySplitting asserts it.
We tried the identity route first, and measured it failing: five real Proof of Humanity registrations from 2021 all proved to Creditcoin successfully, and none of those humans are registered today. That is the precompile working as documented — it proves that an event occurred, not that a state holds. For a repayment those coincide. For an identity they do not.
docs/THREAT_MODEL.md states what this still does not solve, starting with the fact that there is no enforcement layer.
| Real mainnet credit | docs/evidence/g5-mainnet-credit/ — an Ethereum wallet's Aave history becomes a Creditcoin credit line |
| Whole history in one transaction | 0xffad0a92… — 9 Sepolia transactions verified together, 1,207,503 gas, 134,167 per event |
| A real loan, opened and closed | 0xa416364d… borrow · 0xfcdf732e… repay — 100 tUSD against 130 tCTC at Silver |
| The undercollateralized quote | Read it yourself: collateralRequired(0x76f30e…, 1000e18) returns 850e18, and 1300e18 for the self-dealt wallet. Same call, same pool, different evidence |
| Five attacks rejected | npm run negative-paths — free, no wallet, against the live precompile. results.json |
Batching is 4× cheaper, measured like-for-like on the current registry: 134,167 gas per event batched versus 532,140 proving the same kind of Sepolia event one at a time. The saving is structural — verifying a foreign block means walking a continuity chain back to an attested one, and a batch walks it once for all nine.
A single mainnet proof costs more again (991,536 gas for the 2024 repayment) because its continuity proof carries 980 roots rather than 14. Mainnet history cannot be batched at all: real history spans years, and the prover rejects a batch wider than 1,000 blocks.
| Protocol | Address | What it proves |
|---|---|---|
| Aave V3 Pool | 0x87870Bca…fA4E2 |
Repay, Borrow, LiquidationCall |
| Sparklend Pool | 0xC13e21B6…6BE987 |
Aave V3 fork, identical topic0s |
| ENS Registrar | 0x59E16fcC…0CE547 |
NameRegistered, expiry enforced at ingest |
| Proof of Humanity v2 | 0xa4780958…3E74b2 |
Implemented; honestly decorative |
| Contract | Chain | Address |
|---|---|---|
LoanBook v2 |
Ethereum Sepolia | 0x07AdA5C6…6d41D ✅ Etherscan |
CreditRegistry |
Creditcoin CC3 | 0x4C4381dB…Dddf81 ✅ Blockscout |
CreditTierSBT |
Creditcoin CC3 | 0x326F8806…EddD18 ✅ |
LendingPool |
Creditcoin CC3 | 0xf27877fa…9029C ✅ |
TUSD |
Creditcoin CC3 | 0x26FEEdEC…3B051 ✅ |
EvmV1Decoder (linked library) |
Creditcoin CC3 | 0x2b887101…2EDfa |
ETHEREUM MAINNET (chainKey 3)
┌────────────────────────────────┐
│ Aave V3 · Sparklend · ENS │ CREDITCOIN CC3 TESTNET (chainId 102031)
│ Repay / Borrow / │ ┌─────────────────────────────────────────────────┐
│ LiquidationCall │ │ BlockProver precompile @ 0x…0FD2 │
│ → real third-party capital │ │ ▲ verifyAndEmit (Merkle + continuity) │
└───────────┬────────────────────┘ │ │ ≤10 proofs share one continuity proof │
│ │ │
ETHEREUM SEPOLIA (chainKey 1) │ CreditRegistry.sol (Attestcoin Smart Contract) │
┌────────────────────────────────┐ │ replay guard → verify → decode → route on │
│ LoanBook.sol │ │ (chainKey, emitter) → validate │
│ openLoan / repay │ │ │ │
│ → self-reported, no lender │ │ ▼ │
└───────────┬────────────────────┘ │ CreditProfile → ScoreLib → tier │
│ events │ │ │ │
▼ │ ▼ ▼ │
┌────────────────────────────────┐ │ CreditTierSBT LendingPool │
│ Oracle Worker / the web UI │ │ (ERC-721 + 5192, tier → rate, │
│ wait for attestation → │──▶│ on-chain SVG) capacity → credit line │
│ fetch proof → dry run → │prf└─────────────────────────────────────────────────┘
│ execute / executeBatch │
└────────────────────────────────┘
Mainnet history imports one proof per event — real history spans years and the prover rejects a batch wider than 1,000 blocks. Sepolia history, being contemporaneous, keeps the batch path.
The precompile proves inclusion and continuity — nothing else. It explicitly does not check that a transaction succeeded, which chain it came from, or which contract emitted the log. Those are the registry's job, and each is exercised by a test that fails loudly if removed:
| Check | Attack it stops |
|---|---|
| Replay guard on the derived query id | Resubmitting one genuine repayment to farm score |
(chainKey, emitter) is a registered source |
A look-alike Aave on a chain we do not trust — routing is per-pair, so a Sepolia address cannot impersonate its mainnet namesake |
receipt.receiptStatus == 1 |
A reverted repayment counting as a successful one |
Dispatch on the log's own emitter and topic0 |
Anyone emitting identically-shaped events from their own contract. The caller's declared action is a hint and is never trusted for routing |
| Reserve must be registered to count | An obscure 0-decimal token inflating capacity. An unregistered reserve reverts, so the proof stays retryable rather than being silently consumed for nothing |
No Borrow of the same reserve/account in the same transaction |
A flash loan minting arbitrary capacity with zero capital at risk. It still counts as a repayment; it just proves no capacity |
| One repayment credit per proven transaction | Five Repay logs in one transaction scoring the entire 600-point mainnet term from a single proof |
| Borrower read from the log topic | A relaying worker crediting reputation to itself |
Five attacks were rejected on the live chain — forged merkle root, tampered payload, wrong
source chain, replayed query, oversized batch. Reproduce them for free: npm run negative-paths.
Three tiers of effort. Everything in the first two was re-run from a clean clone on 2026-08-14.
crosscredit.vercel.app is read-only by default. No wallet, no funds, no sign-up. Look up any address in the explorer, including the five below, and every number you see is read live from the deployed registry.
git clone https://github.com/OoJae/crosscredit && cd crosscredit
npm install
forge build && forge test # 214 tests, no network required
cp .env.example .env # public RPC endpoints are already filled in
npm run negative-paths # 5 attacks rejected by the LIVE precompile — free eth_calls
npm run check:chains # re-derives the supported-chain list from live CC3
npm run check:abi # the frontend ABI still matches the compiled contracts
npm run prove:mainnet -- --find-aave # list Aave repayments Creditcoin has attested right now
cd web && npm install && npm run devnegative-paths is the one worth your time: it submits a forged Merkle root, a tampered payload, a
wrong-chain proof, a replayed query and an oversized batch to the real precompile on CC3, and shows
you all five being refused. It costs nothing and needs no key.
You need tCTC from the Creditcoin Discord token-faucet (/faucet address:0x…, invite at
creditcoin.org); a real ingest costs about 0.0002 CTC. For the Sepolia
side, any public faucet works — the seeded flows need well under 0.05 ETH.
npm run prove:mainnet -- --find-aave # pick any attested Aave repayment
npm run prove:mainnet -- 0x<mainnetTxHash> # prove it — credit lands on whoever the log names
npm run seed:borrower-c # or make fresh Sepolia history (~10 min to attest)
npm run worker:batch -- --borrower 0x… # and import it all in ONE transactionThis is the honest self-serve demo. Take any real Aave repayment from the list, prove it, and watch a wallet that has never touched Creditcoin acquire a credit profile. Ingestion is permissionless and the borrower is read from the proven log, so you can import anyone's history — and nobody can import it as themselves.
- Importing mainnet history is CLI-only. The app's Import tab reads Sepolia
LoanBookhistory for the address being viewed; the mainnet path lives innpm run prove:mainnet. A judge wanting to exercise the headline capability should use the CLI above. Putting it in the UI is the first thing on the list after submission. npm run poh:negativeneeds an archive RPC and is therefore flaky. It reads Ethereum state from 2021 and rotates through four public endpoints; some of them now gate archive requests (403 Archive requests require a personal token), so whether it works depends on which one answers. Two clean-clone runs on the same afternoon gave one failure and one success. SetMAINNET_RPC_URLto an archive provider (Alchemy's free tier is enough) to make it reliable, or read the captured result indocs/evidence/g5-identity-negative/. It now fails with that instruction rather than a stack trace.
contracts/ Foundry (solc 0.8.30) — 214 tests
src/sepolia/ LoanBook — the credit-history source
src/creditcoin/ CreditRegistry (ASC), ScoreLib, SourceKinds, CreditTierSBT, LendingPool, TUSD
src/vendored/ Attributed upstream base contracts (see below)
test/ Unit tests, mocks, real captured proof fixtures
worker/ Oracle worker — watch, backfill and batch modes
web/ The frontend deployed at crosscredit.vercel.app
scripts/ check-chains, seed-history, capture-proof, negative-paths, prove-mainnet,
poh-negative, deploy
docs/ ATTESTCOIN_INTEGRATION · SCORING · THREAT_MODEL · evidence/
- docs/ATTESTCOIN_INTEGRATION.md — how the protocol is used, what we had to establish ourselves, and where the depth is
- docs/SCORING.md — the model, and why every constant is what it is
- docs/THREAT_MODEL.md — the flaw that shaped the design, and the five things still unsolved
- docs/evidence/ — gate-by-gate proof: G0 round-trip, G2 credit loop, G3 batch import, G5 real mainnet credit, negative paths — including the runs that went wrong
contracts/src/vendored/ contains base contracts adapted from Gluwa's official examples,
gluwa/usc-testnet-bridge-examples pinned at commit 4ff9a3bf. Each file names its
upstream source and lists every modification. All other code here is original work written from
Aug 13, 2026 onward.
Built for BUIDL CTC 2026 Fall.
MIT