Proves transaction A executed before transaction B inside an Ethereum block β a fact carried in no payload and readable by no oracle β and makes a relay's bond pay for breaking its no-sandwich promise.
A real Ethereum mainnet sandwich (block 25,764,741, positions 14 β 15 β 16)
ruled on by a bonded contract on Creditcoin CC3, in
one real transaction
β status 1, 1,092,100 gas, 1.456% of MAX_GAS_CAP. The positions are
not hardcoded in the page's code path: they are decoded, on every page load, from the
logs the Attestcoin precompile itself wrote (a committed capture of that same read backs the
page when a live one is unreachable β see below). 145 Foundry tests, 0 failed.
A note on the name. The icon's
41 β 42 β 43is this project's fingerprint β an illustrative three-slot rhythm, not a result. The sandwich that was actually proven on-chain sits at 14 β 15 β 16, and every number in this document, in the tests and on the demo page is that real one. Nothing here presents41/42/43as a live output.
npm install
npm run dev # http://localhost:3000 β no .env, no wallet, no API key, no account
# /judge β the same evidence, written for one readerThe page shows three rows of Ethereum mainnet block 25764741 lighting up in sequence β
14 searcher buy Β· 15 the victim Β· 16 searcher sell β then SandwichProven and the bond paying
out. Those indices are not hardcoded in the page's code path. They are decoded, on every page
load, from the three TransactionVerified logs the Attestcoin precompile itself wrote inside the
receipt of CC3 transaction
0xd136dea0β¦d243810.
A banner above the ledger states which of the two real sources is on screen β a live chain read,
or the committed data/proof-artifact.json capture of that same read if
the public node is unreachable β and there is no third source. There is no demo mode, no mock and no
toggle anywhere in this repository.
Alongside each index the page shows the laterality decode that produced it (RLLLRRRR β 14), the
real sibling hashes it was folded from, and the position the precompile emitted, so the off-chain
and on-chain answers can be seen agreeing rather than asserted to agree.
Verify one position against a complete stranger's API, no clone required:
curl -s https://eth.blockscout.com/api/v2/transactions/0xec3777f9d0e55d03b9caa3a4b8a786dd62e16eeb327a9f1c45dfbc79af618436 | jq .position
# 14A private RPC or block-builder relay sells one promise: route through us and you will not be
sandwiched. When that promise breaks, the victim has a screenshot and the relay has a denial.
The dispute is unresolvable on-chain for one structural reason β the harm is entirely a fact about
ordering, and a transaction does not carry its own position. There is no tx.index field. No
oracle reports it. No eth_call can be proven for it. A sandwich is three transactions that only
become an attack when you can show that one landed between the other two, and that "between" is
exactly what no payload contains.
A relay posts a CTC bond on Creditcoin behind its no-sandwich promise. A victim submits three
Ethereum transaction hashes from the same block. Index41.proveSandwich then:
- Verifies each of the three transactions with
INativeQueryVerifier.verifyAndEmit(...)β three sequential calls in one Creditcoin transaction, sharing one continuity proof. - Recovers each transaction's ordinal position inside its block by calling
INativeQueryVerifier.calculateTxIndex(merkleProof)β a freeviewthat decodes position out of the left/right laterality of the merkle authentication path. Every sibling on the path is one bit; the position is the shape of the proof, not a claimed field. - Asserts the sandwich shape:
frontRunIndex < victimIndex < backRunIndex, that all three legs emitted aSwaplog from the same pool address (PoolNotTouchedotherwise β note this is the log emitter, not the transaction'sto, which legitimately differs across routers), the same searcher as sender on the outer two, and a higher priority fee on the front-run. - Computes harm as the attacker's realized profit β front-run
amountInversus back-runamountOut, both read fromSwaplogs that are inside the proof. Never a counterfactual against a pre-sandwich reserve ratio: Attestcoin commits transaction history, not state, so there is no state to counterfactually compare against, and a contract that claimed otherwise would be lying. - Pays the victim from the relay's bond and marks the claim
processedQueriesso the same sandwich cannot be claimed twice.
Ordering is not in any payload. The merkle authentication path is the position β that is the entire foundation of this contract, and it is the one thing nothing else here can do.
The same flow as plain text (for terminals, and for anyone whose renderer blocks images)
Ethereum mainnet Creditcoin CC3
block 25,764,741 (chainId 102031)
ββββββββββββββββββββ
β 14 searcher buy ββββ
β 15 the victim ββββΌββ 3 proofs ββ Index41.proveSandwich
β 16 searcher sell ββββ (1 shared β
ββββββββββββββββββββ continuity proof) β
ββ 3Γ INativeQueryVerifier.verifyAndEmit
β β TransactionVerified(3, 25764741, 14/15/16)
ββ 3Γ calculateTxIndex(merkleProof) [free view]
β β position, from left/right laterality
ββ assert 14 < 15 < 16
ββ EvmV1Decoder β Swap logs β realized profit
ββ pay victim from the relay's CTC bond
β SandwichProven Β· HarmPaid
Off-chain, the proving pipeline never trusts the party that served it a proof: every bundle is re-encoded from mainnet, re-folded leaf β root, and its continuity proof chained down to a checkpoint Creditcoin already holds β before a single unit of gas is spent.
| Layer | Technology | Why |
|---|---|---|
| Court | Solidity 0.8, contract extending USCBase |
the ruling has to happen inside contract control flow, not be reported to it |
| Verification | INativeQueryVerifier precompile (verifyAndEmit, calculateTxIndex) |
the only on-chain surface that answers "what position was this?" |
| Decoding | EvmV1Decoder deployed library, linked on chain |
Swap logs β realized profit, from the same bytes the precompile verified |
| Tests | Foundry β 145 unit tests, 6 suites | including a 256-leaf exhaustive round-trip of the decode |
| Pipeline | TypeScript + @gluwa/usc-sdk 0.18.0, ethers v6 |
three interchangeable proof sources behind one interface |
| Demo | Next.js 16 (App Router), Tailwind, ShadCN | server component reads the ruling off CC3 before first paint |
| E2E | Playwright β 54 tests, chromium + Pixel 7 | asserts invariants, never the literal indices |
| Chain | Creditcoin CC3 testnet (102031), Blockscout | source-verified contract, so the explorer decodes it itself |
The single published judging criterion for this hackathon is depth of Attestcoin Protocol utilization. This section is the evidence, stated as a table rather than a claim.
The official examples repo has exactly one file that imports the SDK, and the whole exercised
surface is 3 methods on 2 classes (ProofBuilder ctor β waitUntilHeightAttested β getProof,
plus getLatestAttestedHeightAndHash). That is the field's baseline.
index41 makes 36 distinct Attestcoin surfaces load-bearing, 24 of them undocumented. On a clean default run (
npm run prove, zero flags) 30 of them do real work β 33 if you also count the standby proof sources being constructed. All 36 execute across the default and--kill-hostedruns.
The number to hold us to is 30, not 36, and the gap is worth spelling out rather than hiding, because it is exactly the kind of thing a surface count is normally used to smuggle past a reader. The proof layer is a three-rung ladder. On a default run rung 1 answers first, so rungs 2 and 3 are built but never asked a question. That splits the 36 three ways:
And you do not have to take the split from this table. The pipeline counts each surface at the
moment it exercises it and prints the tally, by name, into the transcript it commits β
docs/pipeline-output.txt ends with:
ATTESTCOIN SURFACES EXERCISED THIS RUN: 30
30 did real work Β· 3 constructed but never queried Β· 3 not reached Β· 36 catalogued Β·
21 of the 30 are undocumented
β¦followed by all 36 listed by name under those three headings.
docs/pipeline-output-local-prover.txt prints 32: the
same run with the standby rungs forced to answer and the four proof-gen HTTP surfaces unreachable.
The union of the two runs is all 36 β 30 of them on the default path. The counter lives in
src/surfaces.ts and throws on any id outside the 36-row catalogue, so the
number cannot be padded by a typo. The split, then:
On a zero-flag npm run prove |
Count | Which |
|---|---|---|
| Surfaces that do real work | 30 | everything not listed below |
| Constructed, never queried β the standby rungs | 3 | service.ProofBuilder, raw.RawProofBuilder, SimpleBlockProvider (ctor) |
| Not reached at all β the standby rungs' working calls | 3 | ProofBuilder.getBatchProof, RawProofBuilder.getProof, the implemented BlockProvider |
Total, across default + --kill-hosted |
36 | --kill-hosted removes the hosted sources, so rungs 2 and 3 answer |
So the honest headline is 30 on the default path, 36 across both runs β never 36 unqualified.
Both runs produce the same merkle root and the same ruling; transcripts in
docs/pipeline-output.txt and
docs/pipeline-output-local-prover.txt. "Undocumented"
means absent from docs.creditcoin.org, not "hard to find".
Reconciliation, re-run for this document: every surface below that the project's 325-surface
capability ledger catalogues was checked against it row by row β 32 of 32 rows agree, zero
disagreements, zero symbols missing from the ledger. The ledger is authoritative and the table
below carries its verdicts, not a second opinion. The four proof-gen HTTP endpoints fall outside
that ledger's SDK scope and are classified against the prover's own published OpenAPI spec and the
docs site (2 documented, 2 not). The totals therefore land at 36 surfaces / 24 undocumented /
12 documented β 30 of the 36 doing real work on a default run, and
docs/PIPELINE.md states
the identical numbers.
| # | Surface | Namespace | Documented? | Load-bearing for |
|---|---|---|---|---|
| 1 | PrecompileChainInfoProvider |
chainInfo |
yes | every chain read; also injected into RawProofBuilder |
| 2 | .getSupportedChainByKey |
chainInfo |
no | refuses to run unless chain key 3 really maps to chainId 1 |
| 3 | .getAttestationGenesisHeight |
chainInfo |
no | the provable-history floor; required by the local continuity builder |
| 4 | .getLatestAttestedHeightAndHash |
chainInfo |
no | the attested tip the target block is measured against |
| 5 | .getContinuityBounds |
chainInfo |
no | the parent/child range the continuity proof must span |
| 6 | .waitUntilHeightAttested |
chainInfo |
no | the wait β driven by its fifth parameter extraDelayMs (15,269 ms β 317 ms) |
| 7 | .getAttestationHeightForDigest |
chainInfo |
no | binds a recomputed continuity digest to a real attestation |
| 8 | .getCheckpointForHeight |
chainInfo |
no | β¦or, as here, to checkpoint 0x5492ed3cβ¦d197 at height 25,764,800 |
| 9 | BLOCK_PROVER_PRECOMPILE_ADDRESS |
blockProver |
yes | the verifier address, and the filter for the precompile's own logs |
| 10 | PrecompileBlockProver |
blockProver |
yes | the free preflight, before any gas is committed |
| 11 | .verifySingle |
blockProver |
yes | all three legs dry-run to true before a claim is built |
| 12 | .computeTransactionIndex |
blockProver |
no | position recovered off-chain, cross-checked against the contract's answer |
| 13 | service.ProofBuilder |
proofProvider |
yes | proof-ladder rung 2 β |
| 14 | service.ProofBuilder.getBatchProof |
proofProvider |
yes | proof-ladder rung 2 β the SDK's own untouched batch call β‘ |
| 15 | raw.RawProofBuilder |
proofProvider |
yes | proof-ladder rung 3 β the prover-free path β |
| 16 | raw.RawProofBuilder.getProof |
proofProvider |
no | the call that actually works for a single-block batch (getBatchProof cannot express one) β‘ |
| 17 | raw.blockProvider.SimpleBlockProvider (ctor) |
proofProvider |
no | new SimpleBlockProvider(rpc), wrapped by this repo's CachingBlockProvider β |
| 18 | raw.blockProvider.BlockProvider |
proofProvider |
no | implemented, not just called β 925 mainnet round-trips avoided per local run β‘ |
| 19 | merkle.hashLeaf |
proofProvider |
no | the leaf hash for the independent merkle walk in src/audit.ts |
| 20 | merkle.hashInner |
proofProvider |
no | the walk itself β leaf β root, one bit of laterality per sibling |
| 21 | merkle.computeDigestOf |
proofProvider |
no | folds the continuity digest chain that lands on Creditcoin's checkpoint |
| 22 | getTransactionWithRaw |
encoding |
no | re-encodes the leaf straight from mainnet |
| 23 | abiEncode |
encoding |
no | β¦and compares it byte for byte with what the proof source served |
| 24 | EncodingVersion |
encoding |
yes | V1, for both the re-encode and the local prover |
| 25 | gas.computeGasLimit |
utils |
no | the submitted gas limit β pallet-evm drops precompile revert reasons during estimation |
| 26 | gas.MAX_GAS_CAP |
utils |
no | the 75,000,000 ceiling every claim is held against |
| 27 | gas.gasAsPercentageOfMax |
utils |
no | headroom before and after (it truncates β see the note in src/claim.ts) |
| 28 | hex.bytesInHexString |
utils |
no | calldata size: 17,860 bytes for the ruling |
| 29 | decoder.decodeEvmV1Transaction |
utils |
no | the whole off-chain preflight decode, trackGas included |
| 30 | GET /api/v1/health |
proof-gen API | yes | reported before anything else runs |
| 31 | GET /api/v1/attested-height/{chain_key} |
proof-gen API | yes | the adaptive poller's clock |
| 32 | POST /api/v1/proof-batch/{chain_key} |
proof-gen API | no | the primary proof source β keyed by block position, and no SDK binding exists for it |
| 33 | ErrorResponse.retriable / last_attested_block |
proof-gen API | no | the backoff schedule β probed every run before the poller relies on it |
| 34 | INativeQueryVerifier.verifyAndEmit |
on-chain | yes | three sequential calls inside one Creditcoin transaction |
| 35 | INativeQueryVerifier.calculateTxIndex |
on-chain | no | the product β ordinal position, from merkle laterality |
| 36 | EvmV1Decoder (deployed library, 9 public selectors) |
on-chain | yes | linked into Index41 on chain; also called off-chain by the preflight |
36 rows Β· 12 documented Β· 24 undocumented Β· 30 doing real work on a zero-flag default run.
β constructed on a default run but never queried β the hosted rung answers first.
β‘ not reached on a default run at all; --kill-hosted is what forces them.
The other 30 rows execute on every run, no flags.
Per-surface reasoning, measurements and the deliberate exclusions: docs/PIPELINE.md.
Remove Attestcoin from this repository and the product does not degrade; it stops existing. The replacement bill is worth itemising, because it is unusually concrete.
There is no oracle for ordering, and there cannot easily be one. A transaction's ordinal
position inside its block is not a field anywhere in Ethereum. It is not in the RLP payload, not in
the receipt, not in any log, not returned by eth_call, and not derivable from anything the
transaction itself commits to. It exists in exactly one place: a block's transactions are committed
as a merkle tree, and a transaction's position is the left/right bit-string of its authentication
path. Every sibling on that path is one bit β left or right β and the concatenation of those bits
is the index in binary. INativeQueryVerifier.calculateTxIndex reads that bit-string on chain,
for free, as a view. Nothing else in the entire Attestcoin surface area answers this question, and
nothing outside it answers the question at all. This is why the headline surface of the project is
an undocumented one: the fact index41 is built on is a fact that only a merkle proof can carry.
The alternative is a committee, and its output is a signature, not a proof. The honest
substitute is an oracle network willing to attest arbitrary historical Ethereum transactions β
not prices, not a feed, but "transaction 0xec37β¦ was the 14th transaction of block 25,764,741".
No production oracle network sells that product, so you would be standing one up yourself: choosing
signers, bonding them, defining slashing, and running the whole thing for the lifetime of every
bond your contract underwrites. And when it is finished, Index41 would be verifying an ECDSA
signature over a claim. The contract would be trusting the committee's word about Ethereum. Today
the contract re-derives the fact from the block's own merkle commitment and reverts if it does not
hold. Those are different security models, and only one of them survives the committee being wrong.
You would also need a bespoke indexer to make individual fields checkable in a contract.
Ordering alone does not make a sandwich. The claim also requires that all three transactions hit the
same pool β which means a Swap log emitted by that pool inside each leg's proven receipt, not a
matching to address, since the three legs in the demo route through different contracts β that the
outer two share a sender, that the front-run paid the higher priority
fee, and β for harm β the amountIn and amountOut of the Swap logs the attacker actually
emitted. Today those come out of the same verified bytes through EvmV1Decoder's selectors and
utils.decoder.decodeEvmV1Transaction, so every field the contract branches on is inside the thing
the precompile verified. Without Attestcoin, each of those fields is a separate assertion a separate
service must make, and every one of them is a new place to lie. The economically interesting part β
harm is the attacker's realized profit, so paying it out cannot exceed what was proven β depends on
the log values and the ordering being one proof rather than several correlated claims.
And a bridge, and it would still be the weakest link. Getting any of this from Ethereum to Creditcoin without Attestcoin means a message bridge with its own validator set, its own liveness assumptions and its own honest-majority assumption, sitting underneath a contract whose entire selling point is that it does not take anyone's word for anything.
The surfaces are forced by the design, not sprinkled on top. The pipeline never trusts the party
that served it a proof, and that single decision forces most of the table: encoding.getTransactionWithRaw
plus abiEncode to re-encode the leaf from mainnet and diff it byte for byte; merkle.hashLeaf and
merkle.hashInner to re-fold leaf β root independently; merkle.computeDigestOf chained across the
60 continuity roots to a digest that chainInfo.getCheckpointForHeight confirms Creditcoin already
holds at height 25,764,800. That last step binds an off-chain blob to on-chain state, off-chain and
for free β and it is what makes swapping the prover a non-event rather than a leap of faith. The
same refusal-to-trust produced the third proof source, RawProofBuilder over an implemented
BlockProvider, which rebuilds the block's merkle tree from mainnet with no proof service in the
loop at all and reproduces the hosted root byte for byte.
None of these are calls added to lengthen a list. Delete any one of them and something in the pipeline stops being checkable: the leaf becomes unverified, the root becomes hearsay, the continuity proof becomes unanchored, or the claim becomes un-runnable when the hosted prover is down.
Depth is not surface count for its own sake, so three tempting families were left out on purpose:
- The 62-surface
queryBuilderselector API.EvmV1Decoderalready returns sender, entry point, value, nonce, gas limit, receipt status, gas used and the full log set from the same bytes the precompile verified. Spending a selector budget on facts that are already free would be decoration. proofProvider.mergeProofs. It throws on non-contiguous ranges, and three legs of one block are not a range at all.PrecompileBlockProver.verifyBatch/verifyAndEmitBatch. TypeScript-side only β there is no on-chain batch verify, which is exactly why a claim makes three sequentialverifyAndEmitcalls in one transaction and why the gas gate mattered on day 3.
A real Ethereum mainnet sandwich, ruled on by a bonded contract on Creditcoin. The contract is source-verified, so Blockscout decodes the calls and events itself.
| Index41 (source verified) | 0xb37Bc52b9d6f7431Ba8Be4deD4f53281Efb10eC2 Β· CC3 testnet, chainId 102031 |
The ruling β 3Γ verifyAndEmit in one transaction |
0xd136dea0β¦d243810 Β· status 1 Β· block 5,317,821 Β· 5 logs |
| Source of truth | Ethereum mainnet block 25,764,741 β 240 transactions, a real MEV sandwich |
| Positions recovered | 14 β 15 β 16 (searcher buy Β· victim Β· searcher sell), from merkle-path laterality via calculateTxIndex |
| Off-chain vs on-chain | RLLLRRRR β 14 Β· LLLLRRRR β 15 Β· RRRRLRRR β 16 β the local decode and the precompile's emitted index agree on all three |
| Ordering assertion | front 14 < victim 15 < back 16 β holds |
| Harm paid from the bond | 219,708 wei β the address the proof says was sandwiched. Paid == computed. |
| Gas | 1,092,100 β 1.456% of MAX_GAS_CAP (75,000,000) for 3Γ verifyAndEmit + 3Γ calculateTxIndex + the ordering assert + the events |
| Day-one spike | OrderProbe.proveOrder returned [14, 15, 16] live from the real precompile β 292,376 gas, 0.390% of the cap (docs/spike-output.txt) |
| Contract tests | 145 Foundry unit tests, 6 suites, 0 failed |
Reproduce the whole packet in one command β no key, no .env, no wallet:
npm install && npm run capture:checkIt re-reads the CC3 receipt, Ethereum mainnet block 25764741 and the prover live, re-derives the
three positions, and diffs the result against the artifact committed in this repository:
CC3 receipt block 5317821 Β· status 1 Β· 5 logs
ETH block 25764741 Β· 240 transactions
PROVER proof-batch answered in 1216ms Β· cached=false
mainnet agrees: 0xec3777f9d0β¦ is at position 14
mainnet agrees: 0x7b054188f7β¦ is at position 15
mainnet agrees: 0xb0cae362c6β¦ is at position 16
--check: committed artifact MATCHES a fresh live capture
Deploy tx, bond tx, before/after balances and every explorer link β including the three real mainnet
transactions the ruling is over: docs/DEPLOYMENT.md. How the proof is built,
audited and made prover-independent: docs/PIPELINE.md.
145 Foundry unit tests, 0 failed:
Index41MechanismTest 20 wiring, constants, position recovery from merkle laterality
Index41BondTest 27 posting/accumulating a bond, declaring coverage, the unbond clock
Index41ClaimTest 53 the sandwich-shape assertion and every way to fail it
Index41HarmTest 24 realized-profit accounting, replay protection, double-claim guards
OrderProbeTest 17 the day-one spike contract, end to end
USCBaseExecuteTest 4 the USC base class's own execute() guards
| Layer | Tool | Result on this repo |
|---|---|---|
| Contracts | Foundry Β· forge fmt --check |
145 tests, 6 suites, 0 failed Β· formatter clean |
| Exhaustive verification | Foundry | 256 positions β every leaf of the depth-8 tree round-tripped |
| Latency | npm run bench β p50/p95 over repeated real runs |
three paths, correctness-gated Β· DEMO.md |
| Deployed-code identity | node scripts/verify-bytecode.mjs |
every court runs byte-identical executable code |
| Code quality | ESLint 9 (next/core-web-vitals + next/typescript) Β· tsc --noEmit |
clean at --max-warnings=0 |
| E2E | Playwright, chromium + Pixel 7 | 54 tests β zero config, no .env, no wallet |
| Performance | Lighthouse CI over / and /judge |
perf 100 Β· a11y 96 Β· best-practices 100 Β· SEO 100 |
| Security β SAST | CodeQL (javascript-typescript, security-and-quality) |
weekly + every push/PR |
| Security β secrets | gitleaks over the full git history + working tree, TruffleHog | no leaks found |
| Security β SCA | npm audit (production tree, blocking) + Dependabot |
0 vulnerabilities in the shipped tree |
| CI/CD | GitHub Actions β 6 stages, parallel, concurrency-cancelled | contracts + TypeScript + Next, all three gated |
The latency numbers are repeated-run, not one lucky shot. npm run bench proves the same
committed sandwich over and over against the real prover, the real Ethereum RPCs and the real CC3
testnet β no offline mode, no mock, and every trial correctness-gated on positions 14 / 15 / 16, one
shared merkle root, a status-1 receipt and paid == computed. It reports p50 and p95 for the hosted
proof fetch, the local prover-free proof build and the full prove-to-ruling path, and it exits
non-zero if any check fails. Numbers, methodology and limitations: DEMO.md.
The one exhaustive test rather than an example:
test_TxIndexOfRoundTripsEveryPositionInTheTree walks all 256 leaves of the depth-8 tree and
asserts the laterality decode round-trips to the position for every one. The decision function that
must never be wrong is verified over its whole input space, not on three cases.
Test names say what they actually assert. Index41MechanismTest's three
test_MockDecodesLiveMainnetLateralityTo{Fourteen,Fifteen,Sixteen} tests assert against
MockVerifier's Solidity reimplementation of the laterality algorithm β unit tests run on a bare
EVM, where the real precompile address holds no code β so they prove the mock, and their names say
so. The real precompile was confirmed separately and live on CC3 against the same three paths
(docs/spike-output.txt, and again in
docs/DEPLOYMENT.md).
A note on the E2E suite, because it is the easy place to cheat: none of the 54 tests assert 14,
15 or 16. They assert invariants β that each leg's off-chain laterality decode equals the index
the precompile emitted, that the three positions are strictly increasing, that harm paid equals harm
computed, and that the page names which of the two real sources it used. A test that hard-coded the
indices would still pass against a page that had hard-coded them too, which is precisely the failure
this repository exists to avoid.
Solidity is formatted by forge fmt. TypeScript deliberately is not run through Prettier β the
source is hand-formatted with aligned comment blocks that carry meaning. The formatting gate that
exists is the one that can be met exactly.
Node 20+ and npm. Foundry (forge) only if you want to run the contract suite. Nothing else β
no .env, no wallet, no API key and no account on the default path.
git clone --recurse-submodules <repo-url> # lib/forge-std is a submodule; only `npm test` needs it
npm install
npm run dev # http://localhost:3000 Β· /judge for the one-page argumentAlready cloned without it? git submodule update --init β the demo path above does not care, but
forge resolves forge-std/ from lib/forge-std/src/ and nowhere else.
node scripts/capture-proof.mjs --check # re-read every live source, diff the committed artifact
# β "committed artifact MATCHES a fresh live capture"The full on-chain run needs a funded CC3 key at ~/.config/creditcoin/index41-testnet.json, which
is read at runtime and is never in this repository:
npm run build:cc3 # links Index41 against Creditcoin's deployed EvmV1Decoder
npm run prove -- --fresh-court # deploy β bond β declare coverage β prove β pay, on CC3
npm run prove -- --kill-hosted --fresh-court # the same ruling, with the hosted prover switched offNo flags are required and none of them switch the judged capability on or off β --kill-hosted
only changes which of the three interchangeable proof sources answers. Re-running against a court
that already ruled on this sandwich stops at ALREADY RULED: the replay guard, transcript committed
at docs/pipeline-output-replay.txt.
npm ci
forge build # default profile β unit tests link their own EvmV1Decoder
npm test # forge test --summary β 145 tests, all six suites
npm run test:gas # per-test gas report
npm run build && npm run e2e # Playwright β 54 tests, chromium + mobile, zero config
npm run lint # ESLint over app/ src/ scripts/
npm run typecheck # tsc --noEmit
npm run lighthouse # Lighthouse CI over / and /judge
npm run secrets # gitleaks over the full git history AND the working tree
npm run ci # ESLint + tsc + forge fmt --check + 145 forge tests + npm audit
npm run ci:full # β¦plus the Next production build and the 54-test Playwright suite
make security-scan # the above, plus npm audit and a licence checkcontracts/src/
Index41.sol the court β bonding, coverage, proveSandwich, harm accounting, payout
OrderProbe.sol the day-one spike: does calculateTxIndex really recover position, and do
3Γ verifyAndEmit fit in one tx under MAX_GAS_CAP β before any product code
interfaces/INativeQueryVerifier.sol the precompile interface, exactly as Creditcoin ships it
base/USCBase.sol shared USC-SDK plumbing (kept unmodified from the vendored source)
contracts/test/ 145 Foundry unit tests, 6 suites
src/ the TypeScript proving pipeline (npm run prove)
prove.ts entrypoint β resolve block, wait for attestation, fetch, audit, decode,
dry-run, submit, read back the ruling
proof-sources.ts three interchangeable proof sources behind one interface
caching-block-provider.ts implements the SDK's BlockProvider; back-fills from fetched blocks
audit.ts never trusts a proof source: re-encodes, re-folds, chains continuity
claim.ts builds the claim struct, budgets gas, submits, parses events
court.ts Β· eth.ts Β· config.ts Β· prover-api.ts Β· artifacts.ts Β· log.ts
scripts/
spike.ts the day-one live-network spike (docs/spike-output.txt)
find-sandwich.ts scans real Ethereum mainnet blocks for MEV sandwiches
capture-proof.mjs freezes the ruling into data/proof-artifact.json β and refuses to write
one whose own decode disagrees with the chain
data/sandwich-25764741.json the recorded real mainnet sandwich the default run proves
data/proof-artifact.json the ruling, captured from live sources
app/ the demo surface (Next.js 16, App Router)
page.tsx server component β reads the ruling off CC3 before the first paint
judge/page.tsx /judge β the claim, the click path, the receipt, the limitations
_lib/chain.ts plain JSON-RPC + receipt decoding; no SDK, no wallet, no secret
_components/ProofTheatre.tsx the ledger: three rows of block 25764741 lighting in sequence
api/proof/route.ts re-reads the chain on demand, behind the page's own button
e2e/ 54 Playwright tests β invariants, never literal indices
.github/workflows/ CI (6 stages) Β· CodeQL Β· gitleaks
-
calculateTxIndexrecovering position from merkle laterality, on chain - The three-way ordering assertion
front < victim < back - A real Ethereum mainnet sandwich, ruled on with real CC3 transaction hashes on Blockscout
- Harm as the attacker's realized profit, paid from the bond, replay-guarded
- A prover-free proof path that reproduces the hosted merkle root byte for byte
- Multi-relay registry β cut deliberately; one bonded relay ships
- Historical-claim browser β cut deliberately; one claim, one demo
- Automatic sandwich detection on chain β the caller supplies three hashes;
scripts/find-sandwich.tsfinds real ones off-chain
The web surface is deliberately one page about one ruling: it reads the chain and shows the
decode, and it cannot submit a claim β claiming needs a funded key, which belongs in npm run prove
and not in a judge's browser. Wallet connection is one optional footer button that adds the CC3
network; nothing on the default path touches it.
Disclosed rather than discovered.
- It cannot prove state. Attestcoin commits transaction history β a merkle root over
abiEncode(tx, rx). Post-state is never committed, so there are no proofs overeth_call, storage slots orbalanceOfanywhere in this codebase. The design consequence is concrete and load-bearing: harm is the attacker's realized profit read from provenSwaplogs, never a counterfactual against a pre-sandwich reserve ratio. A contract offering the counterfactual would be unsound, so this one does not offer it. - Writability does not exist. Creditcoin reads Ethereum; it cannot write back. Every mechanism here is one-directional by construction β no round-trips, no acknowledgement path to mainnet.
- There is no on-chain batch verification.
INativeQueryVerifierexposes exactlyverifyAndEmitandcalculateTxIndex;verifyBatchis TypeScript-side only. A claim is therefore three sequentialverifyAndEmitcalls inside one transaction, which is why the gas measurement againstMAX_GAS_CAPwas a day-3 go/no-go gate rather than a footnote. - It does not detect sandwiches on chain. The caller supplies three transaction hashes and the contract rules on them.
- Three of the 145 unit tests prove the mock, not the precompile β and are named accordingly. Unit tests run on a bare EVM where the precompile address holds no code. The real precompile was confirmed live on CC3, separately.
- One bonded relay, one ruling, testnet, unaudited. The deployed contract has ruled once, on the sandwich above. The bond is play money until it is not.
- Ethereum mainnet only (Attestcoin chain key 3). Sandwiches essentially do not occur on Sepolia, and the demo needs a real one.
MIT β see LICENSE.
Built on the Attestcoin Protocol and @gluwa/usc-sdk 0.18.0 for BUIDL CTC 2026 Fall
(DoraHacks), DeFi track. The sandwich is real, and someone really lost the money.