Purpose
This skill enables Claude to help users read from and write to smart contracts on EVM-compatible networks.
Key Concepts
- ABI - Application Binary Interface, defines how to interact with a contract
- View/Pure Functions - Read-only functions that don't modify state
- State-Changing Functions - Functions that modify blockchain state (require gas)
- EOA vs Contract - Externally Owned Account vs Smart Contract
Quick Start
Check if Address is a Contract
import { createPublicClient, http } from 'viem'
import { bsc } from 'viem/chains'
const client = createPublicClient({
chain: bsc,
transport: http()
})
const address = '0x...'
// Get bytecode - contracts have code, EOAs don't
const bytecode = await client.getBytecode({ address })
const isContract = bytecode && bytecode !== '0x'
console.log(`${address} is a ${isContract ? 'contract' : 'wallet (EOA)'}`)
Read Contract Data
import { createPublicClient, http } from 'viem'
import { bsc } from 'viem/chains'
// Example: Read ERC20 token info
const ERC20_ABI = [
{
name: 'name',
type: 'function',
inputs: [],
outputs: [{ name: '', type: 'string' }]
},
{
name: 'symbol',
type: 'function',
inputs: [],
outputs: [{ name: '', type: 'string' }]
},
{
name: 'decimals',
type: 'function',
inputs: [],
outputs: [{ name: '', type: 'uint8' }]
},
{
name: 'totalSupply',
type: 'function',
inputs: [],
outputs: [{ name: '', type: 'uint256' }]
}
]
const client = createPublicClient({
chain: bsc,
transport: http()
})
const tokenAddress = '0x...'
const [name, symbol, decimals, totalSupply] = await Promise.all([
client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'name' }),
client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'symbol' }),
client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'decimals' }),
client.readContract({ address: tokenAddress, abi: ERC20_ABI, functionName: 'totalSupply' })
])
console.log(`Token: ${name} (${symbol})`)
console.log(`Decimals: ${decimals}`)
console.log(`Total Supply: ${totalSupply}`)
Write to Contract
import { createWalletClient, createPublicClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { bsc } from 'viem/chains'
const account = privateKeyToAccount('0x...')
const walletClient = createWalletClient({
account,
chain: bsc,
transport: http()
})
const publicClient = createPublicClient({
chain: bsc,
transport: http()
})
// Example: Call a custom contract function
const CUSTOM_ABI = [
{
name: 'stake',
type: 'function',
inputs: [{ name: 'amount', type: 'uint256' }],
outputs: []
}
]
const hash = await walletClient.writeContract({
address: '0xContractAddress...',
abi: CUSTOM_ABI,
functionName: 'stake',
args: [parseEther('100')]
})
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash })
console.log('Transaction confirmed:', receipt.status)
MCP Tools
| Tool | Description |
|---|---|
is_contract |
Check if address is contract or EOA |
read_contract |
Call view/pure functions |
write_contract |
Call state-changing functions |
Common Patterns
Multi-Call (Read Multiple Values)
import { createPublicClient, http } from 'viem'
import { bsc } from 'viem/chains'
const client = createPublicClient({
chain: bsc,
transport: http()
})
// Read multiple values in one call
const results = await client.multicall({
contracts: [
{
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'name'
},
{
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'symbol'
},
{
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'decimals'
}
]
})
console.log(results)
Simulate Transaction Before Sending
// Simulate the transaction to check if it would succeed
const { result } = await publicClient.simulateContract({
address: contractAddress,
abi: CONTRACT_ABI,
functionName: 'someFunction',
args: [arg1, arg2],
account: account.address
})
console.log('Simulation result:', result)
// If simulation succeeds, execute the actual transaction
const hash = await walletClient.writeContract({
address: contractAddress,
abi: CONTRACT_ABI,
functionName: 'someFunction',
args: [arg1, arg2]
})
Decode Transaction Data
import { decodeFunctionData } from 'viem'
const txData = '0x...' // Transaction input data
const decoded = decodeFunctionData({
abi: CONTRACT_ABI,
data: txData
})
console.log('Function:', decoded.functionName)
console.log('Args:', decoded.args)
Encode Function Call
import { encodeFunctionData } from 'viem'
const data = encodeFunctionData({
abi: CONTRACT_ABI,
functionName: 'transfer',
args: ['0xRecipient...', parseEther('100')]
})
console.log('Encoded data:', data)
Working with ABIs
Minimal ABI
You don't need the full ABI - just the functions you're calling:
// Only include what you need
const MINIMAL_ABI = [
{
name: 'balanceOf',
type: 'function',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }]
}
]
Human-Readable ABI
Viem supports human-readable format:
import { parseAbi } from 'viem'
const abi = parseAbi([
'function balanceOf(address account) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'event Transfer(address indexed from, address indexed to, uint256 value)'
])
Security Warnings
- Verify contract source - Check on block explorer before interacting
- Understand what you're signing - Review function calls carefully
- Test on testnet first - Avoid costly mistakes
- Be careful with approvals - Unlimited approvals are risky
- Check for proxy contracts - Implementation may be different
Error Handling
try {
const result = await client.readContract({
address: contractAddress,
abi: CONTRACT_ABI,
functionName: 'someFunction'
})
} catch (error) {
if (error.message.includes('execution reverted')) {
console.log('Contract reverted - check your inputs')
} else if (error.message.includes('could not coalesce')) {
console.log('ABI mismatch - verify contract ABI')
} else {
console.log('Error:', error.message)
}
}