SPARK
[ Measured on mainnet ]

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.

Measured on Ethereum mainnetRead live from archive RPC. Reproducible with the commands at the bottom of this page.
Engine code, executed locallyThirteen Solidity contracts, 518 Foundry tests, run end to end on a local chain using real mainnet data.
Broadcast to Creditcoin CC3 testnetThe stack below is deployed and its limit is read live from the chain in section 06. This is post-deadline work and is not part of the frozen submission.

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.

ReserveEventsNaive sum (events)Real stateError
WETH8497.389032.3249-66.8084%
USDC13249617.428435001.5261-29.4572%
WBTC170.19160.0051-97.3342%
LINK62-0.06640-100.0000%
USDT16000%

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.

WalletAnchor blockBlocksLedgerReal balanceResidual
0x9205…f0bb25,973,5902950.00000.0000 bps
0xb99a…bcf525,883,19790,68813001.813713005.11972.54 bps
0xe904…672f25,925,57348,3122186.49992187.08072.65 bps
0xc79b…2be725,965,9077,9782163.01652163.11160.43 bps
0x13ff…55b725,823,388150,4970.00010.00011.07 bps
0xf15d…4e4325,893,95379,9321390.17581390.78304.36 bps
0xf78e…e2f325,929,38644,4991199.99991200.29392.44 bps
0xf6c6…770025,896,64577,240785.5500785.88154.22 bps
0x7cd0…c91225,957,26416,621614.3999614.45640.91 bps
0x800f…955f25,961,14512,740602.6001602.64220.69 bps
0x00b9…00b925,925,52448,361199.9978200.01881.04 bps
0x0cc6…d66625,962,22011,665433.0140433.04180.64 bps
0x7fc8…8c2a25,896,57677,309399.8999400.06894.22 bps
0x4c38…2d0725,913,00060,885299.9999300.10013.33 bps
0x903f…caae25,852,998120,887122.0000122.00420.34 bps
0x9f13…cfe325,865,118108,767830.1512830.40883.1 bps
0x1213…f4cb25,897,28276,603199.9999200.08374.18 bps
0x1302…4d5c25,954,77019,115189.9999190.02001.05 bps
0x9217…686725,963,30510,580176.9946177.00420.53 bps
0x2f1f…40c325,792,291181,5942560.53152561.39233.36 bps
0x6a01…fbcc25,918,22155,664109.8079109.84153.05 bps
0xaf04…e5b625,954,75119,134105.9999106.01121.05 bps
0x2a89…229125,948,68325,20296.399996.41341.39 bps
0x4f20…dde625,836,666137,219186.3263186.36331.98 bps
0xb0f3…addd25,864,335109,5501.61371.61370.22 bps
0x0760…889025,940,84533,04089.920089.93641.82 bps
0xd255…01d925,924,62049,26585.212685.22561.53 bps
0xa3c8…538025,940,62733,25884.100084.11541.83 bps
0xe7cd…d4b525,937,19736,68883.000383.01621.91 bps
0x29dd…bcae25,923,22350,66280.568480.59082.78 bps
0xadbd…981025,804,052169,83378.117378.12330.76 bps
0xc8b9…ac8725,970,0103,87577.000077.00110.14 bps
0xa818…517225,926,16947,71674.999975.01962.62 bps
0xbf80…014125,920,16153,72474.999975.02212.95 bps
0x8f10…f99625,697,242276,6430.00000.0000 bps
0xa170…54e625,804,452169,433200.0678200.11492.35 bps
0xaa5c…6e2425,913,22260,66359.599959.61983.32 bps
0x441f…ebfe25,899,36274,52353.930053.95194.06 bps
0x85de…fd2f25,898,88874,99751.199951.22094.09 bps
0x2f7d…5f3225,898,85475,03150.000050.02044.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

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

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)