Mainnet Position Proof
Ledger reconstruction of real Aave V3 positions, from real mainnet data
Credit decisions need history that lives on another chain. The obvious way to get it is to sum a protocol's events. We measured that approach against reality and it is wrong by up to 66.8084% on a single position. This page is the measurement, the replacement primitive, and the proof that the replacement reconciles.
Positions reconciled
40
38 within 10 bps
Largest residual
4.36 bps
interest events cannot supply
Mainnet RPC calls
1613
to block 25,973,885
End-to-end gas
3,572,773
local anvil 31337, 10 transactions
01The finding: event summing understates a position
A real mainnet borrower with 311 pool events. The naive method sums Supply minus Withdraw per reserve. The right column is the same position read from protocol state at the same block.
| Reserve | Events | Naive sum (events) | Real state | Error |
|---|---|---|---|---|
| WETH | 84 | 97.3890 | 32.3249 | -66.8084% |
| USDC | 132 | 49617.4284 | 35001.5261 | -29.4572% |
| WBTC | 17 | 0.1916 | 0.0051 | -97.3342% |
| LINK | 62 | -0.0664 | 0 | -100.0000% |
| USDT | 16 | 0 | 0 | 0% |
The WETH row is the clearest case: 97.3890 from events against 32.3249 in reality, with zero Withdraw events in the entire history. The position moved by plain ERC20 Transfer, which emits no Aave event at all. The USDT debt row shows the same class of failure in the other direction: 33300 borrowed from events against 0 actual.
02The replacement: the token's own ledger, anchored at a proven zero
The primitive is the aToken's Transfer ledger, not the pool's events. A ledger sum is only meaningful if you know where it started, so the engine finds the most recent block where balanceOf returns exactly zero and anchors there, which makes coverage complete by construction rather than by assumption.
| Wallet | Anchor block | Blocks | Ledger | Real balance | Residual |
|---|---|---|---|---|---|
| 0x9205…f0bb | 25,973,590 | 295 | 0.0000 | 0.0000 | bps |
| 0xb99a…bcf5 | 25,883,197 | 90,688 | 13001.8137 | 13005.1197 | 2.54 bps |
| 0xe904…672f | 25,925,573 | 48,312 | 2186.4999 | 2187.0807 | 2.65 bps |
| 0xc79b…2be7 | 25,965,907 | 7,978 | 2163.0165 | 2163.1116 | 0.43 bps |
| 0x13ff…55b7 | 25,823,388 | 150,497 | 0.0001 | 0.0001 | 1.07 bps |
| 0xf15d…4e43 | 25,893,953 | 79,932 | 1390.1758 | 1390.7830 | 4.36 bps |
| 0xf78e…e2f3 | 25,929,386 | 44,499 | 1199.9999 | 1200.2939 | 2.44 bps |
| 0xf6c6…7700 | 25,896,645 | 77,240 | 785.5500 | 785.8815 | 4.22 bps |
| 0x7cd0…c912 | 25,957,264 | 16,621 | 614.3999 | 614.4564 | 0.91 bps |
| 0x800f…955f | 25,961,145 | 12,740 | 602.6001 | 602.6422 | 0.69 bps |
| 0x00b9…00b9 | 25,925,524 | 48,361 | 199.9978 | 200.0188 | 1.04 bps |
| 0x0cc6…d666 | 25,962,220 | 11,665 | 433.0140 | 433.0418 | 0.64 bps |
| 0x7fc8…8c2a | 25,896,576 | 77,309 | 399.8999 | 400.0689 | 4.22 bps |
| 0x4c38…2d07 | 25,913,000 | 60,885 | 299.9999 | 300.1001 | 3.33 bps |
| 0x903f…caae | 25,852,998 | 120,887 | 122.0000 | 122.0042 | 0.34 bps |
| 0x9f13…cfe3 | 25,865,118 | 108,767 | 830.1512 | 830.4088 | 3.1 bps |
| 0x1213…f4cb | 25,897,282 | 76,603 | 199.9999 | 200.0837 | 4.18 bps |
| 0x1302…4d5c | 25,954,770 | 19,115 | 189.9999 | 190.0200 | 1.05 bps |
| 0x9217…6867 | 25,963,305 | 10,580 | 176.9946 | 177.0042 | 0.53 bps |
| 0x2f1f…40c3 | 25,792,291 | 181,594 | 2560.5315 | 2561.3923 | 3.36 bps |
| 0x6a01…fbcc | 25,918,221 | 55,664 | 109.8079 | 109.8415 | 3.05 bps |
| 0xaf04…e5b6 | 25,954,751 | 19,134 | 105.9999 | 106.0112 | 1.05 bps |
| 0x2a89…2291 | 25,948,683 | 25,202 | 96.3999 | 96.4134 | 1.39 bps |
| 0x4f20…dde6 | 25,836,666 | 137,219 | 186.3263 | 186.3633 | 1.98 bps |
| 0xb0f3…addd | 25,864,335 | 109,550 | 1.6137 | 1.6137 | 0.22 bps |
| 0x0760…8890 | 25,940,845 | 33,040 | 89.9200 | 89.9364 | 1.82 bps |
| 0xd255…01d9 | 25,924,620 | 49,265 | 85.2126 | 85.2256 | 1.53 bps |
| 0xa3c8…5380 | 25,940,627 | 33,258 | 84.1000 | 84.1154 | 1.83 bps |
| 0xe7cd…d4b5 | 25,937,197 | 36,688 | 83.0003 | 83.0162 | 1.91 bps |
| 0x29dd…bcae | 25,923,223 | 50,662 | 80.5684 | 80.5908 | 2.78 bps |
| 0xadbd…9810 | 25,804,052 | 169,833 | 78.1173 | 78.1233 | 0.76 bps |
| 0xc8b9…ac87 | 25,970,010 | 3,875 | 77.0000 | 77.0011 | 0.14 bps |
| 0xa818…5172 | 25,926,169 | 47,716 | 74.9999 | 75.0196 | 2.62 bps |
| 0xbf80…0141 | 25,920,161 | 53,724 | 74.9999 | 75.0221 | 2.95 bps |
| 0x8f10…f996 | 25,697,242 | 276,643 | 0.0000 | 0.0000 | bps |
| 0xa170…54e6 | 25,804,452 | 169,433 | 200.0678 | 200.1149 | 2.35 bps |
| 0xaa5c…6e24 | 25,913,222 | 60,663 | 59.5999 | 59.6198 | 3.32 bps |
| 0x441f…ebfe | 25,899,362 | 74,523 | 53.9300 | 53.9519 | 4.06 bps |
| 0x85de…fd2f | 25,898,888 | 74,997 | 51.1999 | 51.2209 | 4.09 bps |
| 0x2f7d…5f32 | 25,898,854 | 75,031 | 50.0000 | 50.0204 | 4.09 bps |
40 wallets, positions from 50.0204 to 0.0000 in aEthWETH. 1 matched exactly and 38 of 40 landed within 10 bps, with a largest residual of 4.36 bps.
Three honest corrections, all found by measuring
Exact equality was our first claim and it was wrong: it came from a single wallet with a near-zero balance, where matching is trivial. Across 40 real positions, nothing matches exactly, because interest rebases into the aToken between events. The honest claim is the residual magnitude, and that number is what the on-chain interestResidual bound encodes. Separately, 7 of 40 walked 1715.1398 between wallets, which no event-only method could ever reconstruct. That row is highlighted above.
The third correction came only from scaling this measurement from 8 wallets to 40. One of the 40 holds 2 wei of aEthWETH, so a one-wei ledger difference printed as -5000 bps and briefly became the largest-residual headline on this page. A percentage against a two-wei denominator is an artifact rather than a measurement, so the metric now applies a dust floor and counts excluded wallets explicitly instead of dropping them. The raw balances and residuals are untouched; only the derived percentages were recomputed. Eight wallets never hit this case, which is the argument for a corpus over a sample.
03Parity: every topic pinned against the chain, with negative controls
Constants that are wrong fail silently, so each declared signature is checked against real logs in a 10,000 block window and paired with a negative control that must return zero for the method to be credible. 9 of 10 signatures confirmed live.
Declared signatures
- Aave v3 Supply1,845 logslive
- Aave v3 Withdraw1,927 logslive
- Aave v3 Borrow1,197 logslive
- Aave v3 Repay953 logslive
- ERC20 Transfer (aWETH)2,486 logslive
- Chainlink AnswerUpdated (aggregator)36 logslive
- Comet Supply9 logslive
- Comet Withdraw1 logslive
- Morpho SupplyCollateral320 logslive
- Morpho WithdrawCollateral0 logsunseen
Negative controls (must be zero)
- wrong Supply shape vs Aave Pool0
Supply(address,address,address,uint256)
- wrong AnswerUpdated arity vs aggregator0
AnswerUpdated(int256,uint256)
- AnswerUpdated vs the PROXY (emits nothing)0
AnswerUpdated(int256,uint256,uint256)
The proxy that proves nothing
The ETH/USD price needs a Chainlink answer, and the address everyone knows is the proxy. The proxy emitted 0 logs. Its underlying aggregator emitted 36 in the same window, because AnswerUpdated is emitted by the aggregator. An implementation that attests the proxy would prove no price while appearing to succeed. That is a silent failure mode we only found by checking.
04End to end: the same flow the contract runs
The engine then values the position through attested prices. Executed across 10 transactions on local anvil 31337 using the real mainnet inputs above, then read back from the deployed contracts rather than from the script's own console output.
Ledger net
433.0140
aEthWETH
Reconciled position
433.0338
aEthWETH
Attested ETH/USD
$2,508.77
Chainlink, 8dp
Position value
$1,086,382
at block 25,970,521
Gas used
3,572,773
10 transactions
The inputs were re-verified against mainnet independently after the run, and two checks are worth calling out. The anchor block really does hold a zero balance, so coverage starts from a proven point rather than an assumed one. And the attested balance, read at block 25,970,521, returns 433.0338, which is exactly the value the contract reconciled against. That equality was wrong in an earlier revision of the script, which stamped the balance with a block 97 earlier; the mismatch was found by re-reading the chain rather than by rereading the code.
05And the number it produces
Proving a position is only worth anything if something consumes it. The same broadcast deploys a policy layer that sizes a credit limit from the proven net worth, read back off chain with the rest of the stack.
Proven net worth
$1,086,382
reconstructed from mainnet
Policy LTV
20.00%
set explicitly, not implied
Credit limit
$217,276
85% of net worth, at 20%
Decision status
Eligible
0 means a usable limit
A proven position is not seizable collateral
Spark can read an Aave position on Ethereum mainnet and cannot liquidate it from Creditcoin. So this limit is unsecured credit extended against a verified underwriting signal, and the policy cap is hard-coded at half of net worth, far below the 80 to 95 percent the deposit-backed flow uses. Conflating the two would be the most misleading thing in this codebase.
06Read live from Creditcoin testnet, no wallet required
The engine above is now deployed. Every number below is read from those contracts in your browser as the page loads, including the bytecode check, so each one can be pasted into Blockscout and confirmed independently. Nothing on this panel is cached or filled in.
Reading Creditcoin CC3 testnet...
Generation 2 and the CEIP stack, Creditcoin CC3 testnet
- CreditLine (generation 2, balance-sized, no deposit)0xD8cd1d29...ef2682
- AttestedStanding (portable standing)0x8Cc493C5...6D3383
- GroupCredit (group line with vouching)0x4accC2C2...0b11Ff
- MainnetPositionRegistry0x8c718d73...FcE0Dd
- MainnetTokenRegistry0x5B35f79C...0d1E9f
- AttestedPriceFeed0xFE16ea12...7916c2
- PositionValuer0x95847A47...845282
- PositionSizedCredit0xD19E758C...1e9A04
- AttestcoinPaymentVerifier (reused from generation 1)0xF13205Bd...faC130
What the live read proves, and what it does not
It proves the engine runs and returns a limit against a real mainnet position. It does not prove anyone borrowed. The deposit-backed flow in the demo video is generation 1 at 0x2C358501..., and this deployment does not change it.
One detail worth stating rather than leaving to be noticed: the USD figures here differ slightly from section 04, while the position is byte-identical at 433.033875 aEthWETH. That is the point of the design rather than a discrepancy. The price is an attested Chainlink answer stamped at a mainnet block, not a live fetch, so two runs at different blocks produce two prices for the same position. An implementation that read the price live would have no such excuse.
07A verified record, consumed by someone else
The standing registry publishes a proof-anchored record any protocol can read. Until this deployment, nothing read it, which made portability an interface rather than a fact. So a second, independent product was deployed against it: a merchant that defers payment for an item, using its own policy, on a record it did not create. Spark cannot change that policy, and the merchant cannot change what the record says.
Reading the registry and the merchant on Creditcoin CC3...
The portability pair, Creditcoin CC3 testnet
- AttestedStanding (registry, anchored to the live generation)0x88ea1190...57FbEa
- StandingGatedCheckout (a merchant that is not Spark)0x199A3E0e...f2cA11
- CreditLine generation 1 (where the record's evidence lives)0x2C358501...67C742
08What is not true yet
- The generation-2 stack has now been used end to end once: a 0.0199 ETH Sepolia balance attestation opened a 0.00398 ETH limit with no deposit, and the full limit was then drawn from the product itself. That is the first time this path has carried a borrower from proof to credit, and it is still one account. A mechanism that works once is not demand, and one draw is not a credit book.
- Interest is never fabricated from a timestamp or a rate. The only path that moves a position ahead of the ledger is an attested state balance, and the residual is capped and reverts past the cap.
- The sample is aEthWETH only and biased toward recent depositors. Morpho WithdrawCollateral has no observed logs in the window, so that signature is unconfirmed.
- The attestor is trusted to submit already verified values rather than the contract calling the precompile directly. That trust boundary is documented in the threat model.
- The credit limit has no on-chain enforcement path yet. Nothing draws against it, so it is a policy output rather than a funded line.
- The portability pair is exercised by one account acting as both borrower and merchant. It shows the mechanism is consumable by a different contract under a different policy; it does not show two parties using it.
Spark's live product is separate and unaffected. Paying on Sepolia and opening credit on Creditcoin with dual Attestcoin proofs is deployed and working today, with on-chain history you can read on Blockscout.
Reproduce
cd app && TARGET=40 DISCOVERY_CHUNKS=8 node scripts/position-scale.mjs # the 40-wallet corpus # the bare command defaults to TARGET=8 DISCOVERY_CHUNKS=1 and reconstructs 8 wallets, not 40 cd app && node scripts/protocol-topics.mjs # topic parity + controls cd app && node scripts/gen-evidence-module.mjs # regenerate this page's data cd contracts && forge test # 518 tests cd contracts && bash script/deploy-all-cc3.sh # the CC3 broadcast (dry run by default) cd contracts && bash script/standing-consumer-cc3.sh # the portability pair (dry run by default)