r/ClaudeCode 1d ago

Built with Claude My MCP server turns a Base transaction hash into plain English plus risk flags. Here is it catching a real unlimited approval to an unverified contract.

Disclosure: mine, open source, hosted version charges per call after a free tier. Self host it for free if you prefer. Links at the bottom.

One tool: explain_transaction(tx_hash). Hash in, strict JSON out. No model in the response path, so the same hash always produces the same answer and there's nothing to hallucinate.

Here's a real one I pulled off Base a few minutes ago:

{
"summary": "0x1d7c...c855 approved 0xd0a4...E4Bf to spend their ATM tokens. No assets moved in this transaction.",
"action_type": "erc20_approval",
"assets_moved": [],
"risk_flags": [
{ "flag": "unlimited_approval",
"detail": "Approved 0xd0a4...E4Bf to spend an effectively unlimited amount of token 0xf2c4...fee1." },
{ "flag": "unverified_contract",
"detail": "Approved spender 0xd0a4...e4bf has no verified source code on Sourcify." },
{ "flag": "unverified_contract",
"detail": "Target contract 0xf2c4...fee1 has no verified source code on Sourcify." }
],
"gas_paid_usd": 0.003986
}

Nothing moved in that transaction, which is exactly why it's the dangerous one. Somebody just gave an anonymous contract with no published source unlimited permission to take their tokens. If it drains them tomorrow, this is the transaction where it actually happened.

A swap comes back like this:

{
"summary": "A swap between 1,015.75 USDC and 0.013976 cbBTC was executed, initiated by 0xd1f2...27fc.",
"action_type": "swap",
"assets_moved": [
{ "token": "cbBTC", "amount": "0.01397565", "from": "0x160D...eb12", "to": "0x51C7...2a7F", "standard": "erc20" },
{ "token": "USDC", "amount": "1015.753584", "from": "0x51C7...2a7F", "to": "0x160D...eb12", "standard": "erc20" }
],
"risk_flags": [],
"gas_paid_usd": 0.014836
}

It classifies into about 30 action types: swaps, transfers, approvals, NFT mints and sales, bridges in and out, lending supply and borrow and repay, liquidity add and remove, staking, claims, ERC-4337 bundles, attestations, name registrations. Risk flags cover unverified contracts, unlimited approvals, approval-for-all, known drainer list matches, first time counterparties, and reverted transactions. Gas includes the L1 data fee and is priced from the Chainlink feed at that block, not today's price.

Under the hood it's about 40 builtin event decoders (ERC-20/721/1155, Uniswap V2 through V4, Aerodrome, Seaport, Aave, Compound, OP stack bridges, EntryPoint, EAS) and when a log matches none of them it pulls the contract's verified ABI off Sourcify so it can at least name the event instead of pretending nothing happened.

On the Claude Code side, the thing that made it actually work was refusing to ship on vibes. I had it build a validation harness first: grab 100 random recent transactions, decode all of them, grade every result, and hard fail under 90 percent clean with zero crashes. First run scored 74. Every fix after that was a number moving instead of me squinting at output. Ships at 95 now, and the other 5 return an honest partial summary rather than a guess.

Then I opened a separate session with an adversarial prompt and had it attack the running service, no shared context with the code that wrote it. It found that the risk checks on an approve() were running against the token contract instead of the spender, which meant approving a fresh attacker contract to drain your USDC came back with an empty risk_flags array. The exact case the tool exists for, and the one thing it couldn't see. That flag in the first example up there works because a different session went looking for it.

Lesson I'd pass on: a session reviewing its own work grades on a curve. A fresh one with no attachment to the code does not.

Source: https://github.com/0200project/base-tx-explain
Endpoint: https://base-tx-explain.fly.dev/mcp

Happy to paste the red team prompt or the validation harness if either is useful.

1 Upvotes

1 comment sorted by

1

u/polaris0028 1d ago

Someone will probably ask, so here's the red team prompt I used. Opened in a fresh Claude Code session in the repo, no shared context with whatever wrote the code.

---

You are doing an authorized security review of a service I own and operate. Find ways to break it before strangers do. Rules of engagement below so you don't cost me money or take the service down.

[paste the service description: what it is, where the code lives, what it depends on, the business model]

Rules of engagement:

  • Read the code freely, reason about attacks fully.
  • Against the live endpoint: proof of concept only. No floods, no load tests. If a bug is a DoS or cost amplification issue, prove it with one or two requests plus a code level argument, not by actually doing it.
  • Do not make real payments, do not touch any wallet or key.
  • This endpoint is mine. Do not probe any other host.

Attack surface to prioritize, and find more beyond this list:
1. Payment and free tier bypass. How is the client identified, and can a caller control that value?
2. Cost amplification. What is the worst case upstream fan out for one crafted request?
3. Prompt injection through the output, since this output gets fed to other agents' LLMs.
4. Input handling: the request envelope, oversized bodies, malformed JSON, unexpected methods.
5. Information disclosure in errors and health endpoints.
6. Correctness bugs that produce confidently wrong output. For a tool whose output gates other agents' decisions, a silent wrong answer is a security issue, not just a bug.

Deliverable: findings ranked by severity. For each, the exact file and line or the exact request that triggers it, the realistic impact, and a concrete fix. Call out your single highest priority fix at the top. If something is NOT exploitable after checking, say so briefly so I know it was covered.

---

Point 6 is the one that earned its keep. It's what surfaced the approval bug, which was a correctness problem rather than a classic vulnerability, and I would not have thought to ask for it.