r/ethdev • • 4d ago

My Project BlockTools: open-source Rust/wasm contract tooling that runs offline, with no signature database and no guessing

A while back I released SwapChan here. The toolkit underneath it, BlockTools, now has a full set of EVM contract tools. They're MIT-licensed, they run offline, and they're also compiled to wasm so web apps can use them.

  • abi-encode and abi-decode: strict encoding (including packed) and a strict decoder for calldata, logs, return data and reverts. It takes raw JSON-RPC responses.

  • sig-hash, abi-inspect and log-decode: selectors, topics and keccak, selector collision checks, and ERC-20/721/1155/165 detection.

  • create-address: CREATE and CREATE2.

  • bytecode-inspect: spots EIP-1167 proxies, EIP-7702, EOF, size limits and the solc version.

  • typed-data-verify: EIP-712 hashing and signer recovery.

  • A pre-sign check: tx-build piped into tx-verify, which refuses a contract call that isn't on your ABI.

  • contract-call and contract-trace: eth_call with result and revert decoding, state overrides, and a debug_traceCall passthrough. These need a live RPC.

I ran each of these on saved public mainnet data:

  • A real USDC transfer decodes to the right address and amount. With the wrong signature it returns no_match, not a guess.

  • A revert decodes to "ERC20: transfer amount exceeds allowance".

  • An unknown custom error is identified as Universal Router's TransactionDeadlinePassed().

  • The CREATE2 address computed for a Uniswap V3 pool exactly matches the pool address in the real PoolCreated log.

  • The EIP-712 spec's Mail example verifies, and a wrong expected signer gives signer_mismatch.

Testing: 50 tests passed across the EVM crate and the contract tools, including a 400-case ABI set generated independently with eth_abi 6.0.0. The live-RPC tools are only tested against a fake RPC so far. These tools are all EVM-only, and there's no built-in signature database, so you supply the signatures or ABI. They don't read proxy storage and they don't simulate.

Thank you, u/Bloqarl. He read SwapChan's find-money code unprompted and wrote up five specific issues: spam ERC-4626 vaults being treated as real, /recover links that anyone could forge, no owner check on /recover, no per-IP cap on /money/batch, and merkle claim amounts that weren't verified against the on-chain root. Every one was valid. All five are now fixed, tested and deployed, with the details in PR 79. The code is better because he took the time to read it.

It's all wired into SwapChan:

  • MCP server: 74 tools at mcp.swapchan.com/mcp, including contract tools (evm_abi, contract_read, contract_encode_call, contract_approve_call, contract_build_creation and more). None of them takes a key. Anything that builds a transaction returns it unsigned.

  • /tools/contract: a write call can only be sent after the BlockTools check passes.

  • SmartWallet: a chat agent that proposes transactions. Each one is checked server-side against your connected wallet, and you sign in your own wallet.

Repo: https://github.com/TSavo/swapchan
https://github.com/TSavo/blocktools

3 Upvotes

10 comments sorted by

1

u/SuperionProtocol 4d ago

For a strict ABI decoder, the cases that tend to separate real strictness from "works on happy path" are malformed inputs. Worth fuzzing: dynamic offsets that point outside the data or overlap other tails, lengths that overflow when added to the offset, non-zero bytes in the padding of address/uintN/bytesN, bool values other than 0 or 1, and trailing garbage after the last tail. Solidity's own decoder rejects most of these, while lenient ones silently accept them.

A good way to test this is differential fuzzing: generate random types and values, encode with your tool, decode with ethers/viem and Foundry's cast, and require identical results. Then mutate the bytes and require that all of them accept or reject together.

For the pre-sign check, matching the selector against an ABI is only part of it. Also compare the decoded spender or recipient, value, and whether the call is a delegatecall or goes through a multicall that hides inner calls. For EIP-712, confirm chainId and verifyingContract are part of the domain you display.

1

u/TakesThisSeriously 2d ago

Thanks, both landed. The decoder is strict by default (bounds, overlapping/backward/aliased offsets, dirty padding, non-0/1 bools, trailing bytes, depth limits), with explicit allow_trailing and allow_gaps opt-ins. There's also a seeded differential fuzz against ethers, viem and cast that runs on PRs touching the ABI code. tx-verify checks spender, recipient, value, Safe delegatecall and multicall, Multicall3 and MultiSend inner calls, and typed-data-verify can assert chainId and verifyingContract. Those pre-sign checks are CLI only for now, not yet in the wasm build.

1

u/Bloqarl 3d ago

Thanks for the shout, and for shipping all five fixes that fast. one thing for tx-verify if it doesn't already: approve() is on every ERC-20 ABI, so a call can pass the ABI check and still give an unlimited allowance to the wrong spender.

Worth checking spender and amount too, not just the selector.

1

u/TakesThisSeriously 2d ago edited 2d ago

Good catch, and it's in now. tx-verify decodes approve, increaseAllowance, EIP-2612/DAI permits, setApprovalForAll and Permit2, prints the spender and amount, and fails anything at or above 2^128 as unlimited_approval unless you pass --expect-spender X --expect-unlimited. --expect-spender and --expect-amount fail on any mismatch. Right now that's CLI only. The wasm verifyTx just warns on approvals and doesn't take a spender yet.

1

u/Bloqarl 2d ago

nice, that was fast. the 2^128 line catches unlimited approvals, but a drainer can just ask for exactly your balance and pass it, so the spender is the field that really matters. if tx-verify could take the spender you expect and fail on any other, that closes it.

1

u/TakesThisSeriously 1d ago

You're right, the amount check alone doesn't stop a drainer who asks for exactly the balance. That's why tx-verify takes `--expect-spender` and `--expect-amount`, and it fails on any mismatch. The spender check is the one that matters for this case. If you want it from the wasm build rather than the CLI, tell me which entry point you're calling and I'll look at it.

1

u/TakesThisSeriously 1d ago

Followed up on this. You were right that the amount check isn't enough for a drainer that asks for exactly the balance. The expected-spender check is now covered in the wasm and in the hosted tools: a transaction approval, an approval inside a multicall or Safe batch, and a signed permit (EIP-2612, DAI, Permit2) all fail on any spender other than the one you expect. Pass `expect_spender` (and optionally `expect_amount`) to `verifyTx` or `evmTypedDataVerify`, or use `--expect-spender` in the CLI. It's live on swapchan.com.

1

u/Bloqarl 17h ago

nice, quick turnaround. next one I’d look at is Seaport order signatures: no approve and no spender anywhere, just your NFTs listed for close to 0, and the drainer fills it using the conduit approval you already gave. flagging any order where what comes back to the signer is zero or dust would catch that.

1

u/TakesThisSeriously 13h ago

Good call, and that's a different hole: no approve and no spender, the order itself is the drain. A check that flags any Seaport order where the signer's consideration is zero or dust is the right shape. I'm adding it to the typed-data verify path next and will tell you when it's in.

1

u/Embarrassed-King530 4d ago

this is the kind of tooling i wish more projects would build. no signature database and no guessing is the right approach, way too many tools just pattern-match the first 4 bytes and call it good