| name | crypto-recon-web3 |
| description | Specialist for EVM blockchain cryptography — EIP-712, personal_sign, ABI encoding, keccak256, ethers.js/wagmi/viem/web3.js. EVM chains only (Ethereum, Polygon, BSC, Arbitrum, etc.). For non-EVM use crypto-recon-web3-solana, crypto-recon-web3-cosmos, or crypto-recon-web3-starknet. |
Crypto Recon — EVM Blockchain Specialist
Scope: EVM Chains Only
This skill covers Ethereum Virtual Machine (EVM) compatible chains:
Ethereum, Polygon, BSC, Arbitrum, Optimism, Avalanche, Fantom, Base, zkSync, Linea, Scroll, Tron (EVM-compatible mode), and any chain compatible with MetaMask/WalletConnect.
| Non-EVM Chain | Use Instead |
|---|
| Solana, Aptos, Sui, NEAR, TON, Algorand | crypto-recon-web3-solana |
| Cosmos, Osmosis, Polkadot, Substrate | crypto-recon-web3-cosmos |
| StarkNet, ZK-STARK chains | crypto-recon-web3-starknet |
Purpose
Find, map, and reconstruct every cryptographic operation specific to blockchain and Web3 applications — from how messages are hashed before signing, to the exact ABI encoding of contract calls, to the full EIP-712 domain structure. Output enables full replication of wallet interactions and transaction signing.
Do NOT reconstruct Python until the FULL signing flow is traced:
message construction → encoding → hashing → signing → submission.
Missing any step produces wrong signatures.
Complete Pattern Catalog
1. Signing Methods
personal_sign / eth_sign (EIP-191)
await signer.signMessage(message)
await signer.signMessage(ethers.utils.arrayify(messageHash))
await web3.eth.sign(message, account)
await web3.eth.personal.sign(message, account, password)
await ethereum.request({method: 'personal_sign', params: [message, account]})
await ethereum.request({method: 'eth_sign', params: [account, hash]})
await walletClient.signMessage({ account, message })
How EIP-191 works:
prefix = "\x19Ethereum Signed Message:\n" + len(message)
hash = keccak256(prefix + message)
signature = secp256k1.sign(hash, privateKey)
→ returns: {r, s, v} or compact 65-byte hex
signTypedData (EIP-712) — Most Important
await signer._signTypedData(domain, types, value)
await signer.signTypedData(domain, types, value)
await ethereum.request({
method: 'eth_signTypedData_v4',
params: [account, JSON.stringify({ domain, types, primaryType, message })]
})
const { signTypedData } = useSignTypedData()
signTypedData({ domain, types, primaryType, message: value })
await walletClient.signTypedData({ account, domain, types, primaryType, message })
signTypedData({ privateKey, data: { domain, types, primaryType, message }, version: 'V4' })
How EIP-712 works:
domainSeparator = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
keccak256(name),
keccak256(version),
chainId,
verifyingContract
)
)
typeHash = keccak256("Order(address maker,uint256 amount,uint256 deadline)")
structHash = keccak256(abi.encode(typeHash, maker, amount, deadline))
digest = keccak256("\x19\x01" + domainSeparator + structHash)
signature = secp256k1.sign(digest, privateKey)
signTransaction
const tx = { to, value, gasLimit, gasPrice, nonce, chainId }
const signedTx = await wallet.signTransaction(tx)
const signed = await web3.eth.accounts.signTransaction(tx, privateKey)
const request = await walletClient.prepareTransactionRequest({...})
const signedTx = await walletClient.signTransaction(request)
2. EIP-712 Domain Extraction
This is the most important artifact. Find it VERBATIM.
Search patterns:
const domain = {
name: "...",
version: "...",
chainId: ...,
verifyingContract: "0x..."
}
DOMAIN, EIP712_DOMAIN, typedDataDomain, domainData
getDomain(), buildDomain(), createDomain()
_signTypedData({ name: "...", version: "1", chainId: 1, verifyingContract: "0x..." }, ...)
For each domain found, extract:
name (string)
version (string, often "1" or "2")
chainId (number — 1=mainnet, 137=polygon, 42161=arbitrum, 56=BSC, etc.)
verifyingContract (address)
- Any extra fields:
salt, chainId variations
3. EIP-712 Types Extraction
The type definitions define WHAT gets signed.
Search patterns:
const types = {
TypeName: [
{ name: "field1", type: "address" },
{ name: "field2", type: "uint256" },
{ name: "field3", type: "bytes32" },
{ name: "nested", type: "NestedType" },
],
NestedType: [
{ name: "x", type: "uint256" }
]
}
TypedDataEncoder.hash(domain, types, value)
TypedDataEncoder.encode(types, value)
TypedDataEncoder.hashStruct(primaryType, types, value)
For each types definition found, extract completely:
- Primary type name
- All fields with exact Solidity types
- Nested types
- Any array types (
uint256[], bytes32[])
Common Solidity types used:
address, uint8, uint16, uint32, uint64, uint128, uint256
int8, int256
bytes, bytes4, bytes32
string, bool
address[], uint256[]
4. ABI Encoding Patterns
Contract calls encode function arguments before signing/sending.
Search for ABI definitions:
const ABI = [{"type":"function","name":"approve","inputs":[...]}]
const abi = [{"inputs":[{"internalType":"address","name":"spender"...}],"name":"approve"...}]
const iface = new ethers.utils.Interface(ABI)
const contract = new ethers.Contract(address, ABI, signer)
new Interface(abi)
const contract = new web3.eth.Contract(ABI, address)
Search for ABI encoding calls:
iface.encodeFunctionData("transfer", [to, amount])
iface.encodeFunctionData("approve", [spender, MaxUint256])
iface.decodeFunctionResult("balanceOf", data)
iface.parseTransaction({ data })
iface.parseLog({ topics, data })
contract.methods.transfer(to, amount).encodeABI()
contract.methods.approve(spender, amount).encodeABI()
ethers.utils.defaultAbiCoder.encode(["address", "uint256"], [to, amount])
web3.eth.abi.encodeParameters(["address", "uint256"], [to, amount])
abi.encode(...)
For each ABI found:
- Extract the full ABI array verbatim
- Identify which functions are called
- Note the contract address if hardcoded
- Note what the encoded data is used for (sent to where?)
5. Keccak256 Usage
In Web3, keccak256 is everywhere. Classify each usage:
ethers.utils.keccak256(data)
ethers.utils.keccak256(ethers.utils.toUtf8Bytes(str))
ethers.utils.solidityKeccak256(["address", "uint256"], [addr, amount])
ethers.utils.solidityPacked(["address", "uint256"], [addr, amount])
web3.utils.keccak256(data)
web3.utils.soliditySha3(...)
web3.utils.soliditySha3Raw(...)
web3.eth.abi.encodePacked(...)
keccak256(toBytes(data))
keccak256(encodeAbiParameters(...))
keccak256("transfer(address,uint256)").slice(0, 10)
Classify each keccak256 call:
MESSAGE_HASH — hashing a message before signing
DOMAIN_SEPARATOR — EIP-712 domain hash
STRUCT_HASH — EIP-712 struct hash
FUNCTION_SELECTOR — contract function ID (4 bytes)
LEAF_HASH — Merkle tree leaf
TOKEN_ID — NFT ID generation
STORAGE_SLOT — contract storage key
OTHER — unclear purpose
6. Wallet & Key Management
Wallet creation/import:
new ethers.Wallet(privateKey)
new ethers.Wallet(privateKey, provider)
ethers.Wallet.fromMnemonic(mnemonic)
ethers.Wallet.fromMnemonic(mnemonic, path)
ethers.utils.HDNode.fromMnemonic(mnemonic)
ethers.utils.HDNode.fromSeed(seed)
web3.eth.accounts.create()
web3.eth.accounts.privateKeyToAccount(privateKey)
web3.eth.accounts.decrypt(keystore, password)
privateKeyToAccount(privateKey)
mnemonicToAccount(mnemonic)
CRITICAL — Hardcoded key detection:
0x[0-9a-fA-F]{64} ← private key (64 hex chars)
[0-9a-fA-F]{64} ← private key without 0x
PRIVATE_KEY|privateKey|pk ← variable names holding keys
Mnemonic patterns (12 or 24 words from BIP39):
[a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+ [a-z]+
If ANY private key or mnemonic is found hardcoded in JS → CRITICAL finding, extract verbatim.
7. Web3 Libraries Deep Pattern Map
ethers.js (v5 and v6)
ethers.utils.keccak256
ethers.utils.arrayify
ethers.utils.hexlify
ethers.utils.toUtf8Bytes
ethers.utils.formatUnits
ethers.utils.parseUnits
ethers.BigNumber.from
ethers.constants.MaxUint256
ethers.providers.Web3Provider
ethers.providers.JsonRpcProvider
import { keccak256, toUtf8Bytes } from "ethers"
import { Contract, Wallet, JsonRpcProvider } from "ethers"
TypedDataEncoder
wagmi + viem
useAccount, useConnect, useDisconnect
useSignMessage, useSignTypedData
useSendTransaction, useWriteContract
useReadContract, useSimulateContract
useWalletClient, usePublicClient
createWalletClient, createPublicClient
http(), webSocket()
parseAbi, parseAbiItem
encodeFunctionData, decodeFunctionData
encodeAbiParameters, decodeAbiParameters
keccak256, toBytes, fromBytes
web3.js
new Web3(provider)
web3.eth.Contract
web3.eth.accounts.sign
web3.eth.abi.encodeFunction
web3.utils.sha3
web3.utils.toWei, web3.utils.fromWei
@metamask/eth-sig-util
signTypedData({ privateKey, data, version: 'V4' })
recoverTypedSignature({ data, signature, version })
TypedDataUtils.eip712Hash(typedData, version)
TypedDataUtils.hashStruct(primaryType, types, data, version)
8. Permit / EIP-2612
permit allows gasless token approvals via signature.
const domain = { name: tokenName, version: "1", chainId, verifyingContract: tokenAddress }
const types = {
Permit: [
{ name: "owner", type: "address" },
{ name: "spender", type: "address" },
{ name: "value", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
}
const value = { owner, spender, value: amount, nonce, deadline }
const sig = await signer._signTypedData(domain, types, value)
DOMAIN_SEPARATOR = keccak256("EIP712Domain(string name,...)")
PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
digest = keccak256("\x19\x01" + DOMAIN_SEPARATOR + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonce, deadline)))
9. Merkle Proof Patterns
Common in NFT allowlists, airdrop claims.
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true })
const root = tree.getRoot()
const proof = tree.getHexProof(leaf)
MerkleTree.verify(proof, leaf, root, keccak256, { sortPairs: true })
StandardMerkleTree.of(values, ["address", "uint256"])
tree.getProof(leaf)
10. Signature Utilities
ethers.utils.splitSignature(sig)
const { r, s, v } = ethers.utils.splitSignature(sig)
ethers.utils.joinSignature({ r, s, v })
ethers.utils.hexConcat([r, s, vBytes])
ethers.utils.recoverAddress(digest, signature)
ethers.utils.verifyMessage(message, signature)
ethers.utils.verifyTypedData(domain, types, value, signature)
web3.eth.accounts.recover(message, signature)
Analysis Steps
Step 1: Library Identification
Run crypto-recon-libraries first (already done if running post-detection).
Know which library is in use before searching for patterns.
Step 2: Find the Signing Entry Point
Search for the user-facing action that triggers signing:
onClick → signMessage / signTypedData / sendTransaction
Trace backwards from this to find the message/data construction.
Step 3: Extract Message Construction
Before signing, what is built?
Raw data (user inputs)
→ formatted/encoded
→ hashed
→ signed
Document EVERY transformation step.
Step 4: Extract Domain + Types (EIP-712)
If EIP-712 detected:
- Extract domain object verbatim (all fields)
- Extract types object verbatim (all fields and types)
- Note the primaryType
- Note what the
message (value) contains
Step 5: Trace to Wallet Provider
Where does the final sign request go?
ethereum.request({method: 'eth_signTypedData_v4', ...})
signer._signTypedData(domain, types, value)
walletClient.signTypedData(...)
Step 6: Contract Address Inventory
List all contract addresses found in JS:
- Token contracts (ERC-20, ERC-721, ERC-1155)
- DEX routers
- Protocol contracts
- Proxy contracts
Output Format
WEB3 CRYPTO FINDINGS
====================
[W1] EIP-712 Structured Signing — HIGH
File: chunks/abc123.js
Line: 4501
Method: eth_signTypedData_v4 via MetaMask
Domain:
name: "MyDEX"
version: "1"
chainId: 1
verifyingContract: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
Types:
Order: [
{ name: "maker", type: "address" },
{ name: "taker", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "price", type: "uint256" },
{ name: "deadline", type: "uint256" },
{ name: "nonce", type: "uint256" }
]
Message built at: buildOrderMessage():3801
Signing triggered by: submitOrder():5100
Callers: PlaceOrderModal:6200, QuickBuyButton:7100
[W2] permit() EIP-2612 — HIGH
File: vendor-xxx.js (ethers.js bundle)
Line: 14401
Token: USDC (0xA0b8...)
Fields: owner, spender, value, nonce, deadline
Used for: Gasless token approval before swap
[W3] Merkle Proof Verification — MEDIUM
File: chunks/nft-mint.js
Line: 881
Library: @openzeppelin/merkle-tree
Usage: NFT allowlist verification
Pattern: keccak256(abi.encodePacked(address, maxMint))
[W4] Hardcoded Contract Addresses — INFO
USDC: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
Router: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
WETH: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
Network: Ethereum Mainnet (chainId: 1)
Python Reconstruction Notes
When crypto-recon-reconstruct is called for a Web3 finding, use:
from eth_account import Account
from eth_account.structured_data.hashing import hash_domain, hash_message
from eth_account.messages import encode_defunct, encode_typed_data
from web3 import Web3
msg = encode_defunct(text="Hello")
signed = Account.sign_message(msg, private_key=pk)
structured_data = {
"types": {
"EIP712Domain": [...],
"Order": [...]
},
"domain": { "name": "MyDEX", "version": "1", "chainId": 1, "verifyingContract": "0x..." },
"primaryType": "Order",
"message": { "maker": "0x...", "amount": 1000000, ... }
}
signed = Account.sign_typed_data(private_key=pk, full_message=structured_data)
from eth_hash.auto import keccak
result = keccak(data)
from eth_abi import encode
encoded = encode(["address", "uint256"], [to_address, amount])