ethereum-tools
by 0xGval
Verified
- evm-mcp-tools
- tools
// Ethereum Contract Analyzer Tool for MCP Server
import { z } from 'zod';
import axios from 'axios';
import { Web3 } from 'web3';
// Use Etherscan API key from environment variables
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY;
console.error('Etherscan API key set:', !!ETHERSCAN_API_KEY);
// Register for global error handling
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION:', err);
});
/**
* Registers the contract audit tool with the MCP server
* @param {McpServer} server - The MCP server instance
*/
export function registerAuditTool(server) {
console.error('Registering audit tool...');
server.tool("auditContract",
{
address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address"),
chain: z.string().optional().default("eth")
},
async ({ address, chain }) => {
try {
// Use the web3 instance from the environment variable or default public node
console.error('Creating Web3 instance with URL:', process.env.ETH_RPC_URL);
const web3 = new Web3(process.env.ETH_RPC_URL || 'https://eth.llamarpc.com');
// Perform the contract analysis
console.error('Analyzing address:', address);
const result = await analyzeAddress(web3, address);
// Format the output for MCP response
return {
content: [{
type: "text",
text: formatAnalysisResults(result)
}]
};
} catch (error) {
console.error('Error in auditContract:', error);
return {
content: [{
type: "text",
text: `Error analyzing contract: ${error.message}`
}]
};
}
}
);
console.error('Audit tool registered successfully');
}
/**
* Analyze an Ethereum address
* @param {Web3} web3 - Web3 instance
* @param {string} address - Ethereum address to analyze
* @returns {Promise<Object>} Analysis results
*/
async function analyzeAddress(web3, address) {
try {
// Validate address
if (!web3.utils.isAddress(address)) {
throw new Error('Invalid Ethereum address format');
}
// Clean and format the address
const formattedAddress = web3.utils.toChecksumAddress(address);
// Step 1: Check if address is a contract
const isContract = await checkIfContract(web3, formattedAddress);
let result = {
address: formattedAddress,
isContract: isContract,
isVerified: false,
contractName: null,
contractCreator: null,
creationTx: null,
creationTimestamp: null,
contractCode: null,
sourceCode: null,
abi: null,
error: null
};
// If not a contract, return early
if (!isContract) {
result.error = 'Address is not a contract';
// Check ETH balance
const balance = await web3.eth.getBalance(formattedAddress);
result.ethBalance = web3.utils.fromWei(balance, 'ether');
// Check transaction count
const txCount = await web3.eth.getTransactionCount(formattedAddress);
result.transactionCount = txCount;
return result;
}
// Get creation info
const creationInfo = await getContractCreationInfo(formattedAddress);
if (creationInfo) {
result.contractCreator = creationInfo.contractCreator;
result.creationTx = creationInfo.txHash;
result.creationTimestamp = creationInfo.timestamp;
}
// Step 2: Check if contract is verified on Etherscan
const verificationInfo = await checkIfVerified(formattedAddress);
result.isVerified = verificationInfo.isVerified;
// Get bytecode regardless of verification status
const bytecode = await web3.eth.getCode(formattedAddress);
result.contractCode = bytecode;
// Step 3: If verified, get contract code and ABI
if (verificationInfo.isVerified) {
result.sourceCode = verificationInfo.sourceCode;
result.contractName = verificationInfo.contractName;
result.abi = verificationInfo.abi;
// Check contract interfaces/standards (ERC20, ERC721, etc.)
result.standards = await detectContractStandards(formattedAddress, verificationInfo.abi);
} else {
result.error = 'Contract is not verified on Etherscan';
// Attempt to detect contract type from bytecode
result.probableType = await detectContractTypeFromBytecode(bytecode);
}
// Add security analysis if verified (basic heuristics)
if (verificationInfo.isVerified && verificationInfo.sourceCode) {
result.securityAnalysis = analyzeContractSecurity(verificationInfo.sourceCode);
}
return result;
} catch (error) {
console.error('Error analyzing address:', error);
return {
address: address,
isContract: false,
isVerified: false,
contractCode: null,
sourceCode: null,
error: error.message
};
}
}
/**
* Check if an address is a contract
* @param {Web3} web3 - Web3 instance
* @param {string} address - Ethereum address to check
* @returns {Promise<boolean>} True if address is a contract
*/
async function checkIfContract(web3, address) {
try {
console.error('Checking if address is a contract:', address);
const code = await web3.eth.getCode(address);
// If the code is just "0x" or empty, it's not a contract
return code !== '0x' && code !== '0x0';
} catch (error) {
console.error('Error checking if contract:', error);
return false;
}
}
/**
* Get contract creation information from Etherscan
* @param {string} address - Contract address
* @returns {Promise<Object>} Creation information
*/
async function getContractCreationInfo(address) {
try {
const url = `https://api.etherscan.io/api?module=contract&action=getcontractcreation&contractaddresses=${address}&apikey=${ETHERSCAN_API_KEY}`;
const response = await axios.get(url);
const data = response.data;
if (data.status !== '1' || !data.result || !data.result[0]) {
return null;
}
// Get creation transaction info to get the timestamp
const txHash = data.result[0].txHash;
let timestamp = null;
try {
const txUrl = `https://api.etherscan.io/api?module=proxy&action=eth_getTransactionByHash&txhash=${txHash}&apikey=${ETHERSCAN_API_KEY}`;
const txResponse = await axios.get(txUrl);
if (txResponse.data.result && txResponse.data.result.blockNumber) {
const blockNumber = parseInt(txResponse.data.result.blockNumber, 16);
const blockUrl = `https://api.etherscan.io/api?module=block&action=getblockreward&blockno=${blockNumber}&apikey=${ETHERSCAN_API_KEY}`;
const blockResponse = await axios.get(blockUrl);
if (blockResponse.data.status === '1' && blockResponse.data.result) {
timestamp = blockResponse.data.result.timeStamp;
}
}
} catch (error) {
console.error('Error getting transaction timestamp:', error);
}
return {
contractCreator: data.result[0].contractCreator,
txHash: txHash,
timestamp: timestamp
};
} catch (error) {
console.error('Error getting contract creation info:', error);
return null;
}
}
/**
* Check if a contract is verified on Etherscan
* @param {string} address - Contract address to check
* @returns {Promise<Object>} Verification information
*/
async function checkIfVerified(address) {
try {
const url = `https://api.etherscan.io/api?module=contract&action=getsourcecode&address=${address}&apikey=${ETHERSCAN_API_KEY}`;
const response = await axios.get(url);
const data = response.data;
if (data.status !== '1' || !data.result || !data.result[0]) {
return { isVerified: false };
}
const contractData = data.result[0];
// If SourceCode is empty or just a "{}", the contract is not verified
const isVerified = contractData.SourceCode && contractData.SourceCode !== '{}' && contractData.SourceCode.length > 2;
return {
isVerified: isVerified,
contractName: contractData.ContractName,
sourceCode: contractData.SourceCode,
abi: isVerified ? JSON.parse(contractData.ABI) : null
};
} catch (error) {
console.error('Error checking verification status:', error);
return { isVerified: false };
}
}
/**
* Detect contract standards (ERC20, ERC721, etc.)
* @param {string} address - Contract address
* @param {Array} abi - Contract ABI
* @returns {Promise<Object>} Detected standards
*/
async function detectContractStandards(address, abi) {
if (!abi || !Array.isArray(abi)) {
return {
isERC20: false,
isERC721: false,
isERC1155: false
};
}
// Function signatures for different standards
const erc20Functions = ['totalSupply', 'balanceOf', 'transfer', 'transferFrom', 'approve', 'allowance'];
const erc721Functions = ['balanceOf', 'ownerOf', 'safeTransferFrom', 'transferFrom', 'approve', 'getApproved', 'setApprovalForAll', 'isApprovedForAll'];
const erc1155Functions = ['balanceOf', 'balanceOfBatch', 'setApprovalForAll', 'isApprovedForAll', 'safeTransferFrom', 'safeBatchTransferFrom'];
// Extract function names from ABI
const functionNames = abi
.filter(item => item.type === 'function')
.map(func => func.name);
// Check standard compliance by matching function signatures
const isERC20 = erc20Functions.every(func => functionNames.includes(func));
const isERC721 = erc721Functions.every(func => functionNames.includes(func));
const isERC1155 = erc1155Functions.every(func => functionNames.includes(func));
return {
isERC20,
isERC721,
isERC1155
};
}
/**
* Attempt to detect contract type from bytecode
* @param {string} bytecode - Contract bytecode
* @returns {Promise<string>} Probable contract type
*/
async function detectContractTypeFromBytecode(bytecode) {
// This is a simple heuristic approach, more advanced detection would require deeper analysis
// Look for common bytecode patterns
if (bytecode.includes('06fdde03') && bytecode.includes('95d89b41') && bytecode.includes('18160ddd')) {
return 'Likely Token (ERC20/ERC721)';
}
if (bytecode.includes('01ffc9a7')) {
return 'Supports ERC165 Interface Detection';
}
if (bytecode.includes('e8a3d485')) {
return 'Possible Uniswap-related contract';
}
if (bytecode.includes('6080604052')) {
return 'Solidity 0.4.x+ Contract';
}
return 'Unknown Contract Type';
}
/**
* Basic security analysis of contract source code
* @param {string} sourceCode - Contract source code
* @returns {Object} Security issues found
*/
function analyzeContractSecurity(sourceCode) {
const issues = [];
// Check for reentrancy vulnerabilities
if (sourceCode.includes('call.value') && !sourceCode.includes('ReentrancyGuard')) {
issues.push({
severity: 'High',
issue: 'Potential reentrancy vulnerability',
description: 'Contract uses call.value without ReentrancyGuard or checks-effects-interactions pattern'
});
}
// Check for tx.origin usage
if (sourceCode.includes('tx.origin')) {
issues.push({
severity: 'Medium',
issue: 'tx.origin used for authentication',
description: 'Using tx.origin for authentication can be exploited by phishing attacks'
});
}
// Check for unchecked external calls
if ((sourceCode.includes('.call(') || sourceCode.includes('.delegatecall(')) &&
!sourceCode.match(/require\s*\(\s*.*\.call\s*\(/g)) {
issues.push({
severity: 'Medium',
issue: 'Unchecked external call',
description: 'External calls without checking return value can lead to silent failures'
});
}
// Check for use of block.timestamp
if (sourceCode.includes('block.timestamp') || sourceCode.includes('now')) {
issues.push({
severity: 'Low',
issue: 'Timestamp dependence',
description: 'Using block.timestamp for critical logic can be manipulated by miners'
});
}
// Check for self-destruct without access control
if (sourceCode.includes('selfdestruct') || sourceCode.includes('suicide')) {
issues.push({
severity: 'High',
issue: 'Unprotected self-destruct',
description: 'Self-destruct functionality found - ensure it has proper access controls'
});
}
return {
issuesFound: issues.length > 0,
issues: issues
};
}
/**
* Extract function signatures from contract ABI
* @param {Array} abi - Contract ABI
* @returns {Array} Function signatures
*/
function extractFunctionSignatures(abi) {
if (!abi) return [];
return abi
.filter(item => item.type === 'function')
.map(func => {
const inputs = func.inputs?.map(input => `${input.type} ${input.name || ''}`).join(', ') || '';
const outputs = func.outputs?.map(output => `${output.type} ${output.name || ''}`).join(', ') || '';
const stateMutability = func.stateMutability ? `${func.stateMutability}` : '';
return {
name: func.name,
signature: `${func.name}(${inputs})`,
returns: outputs ? `returns (${outputs})` : '',
stateMutability,
visibility: func.visibility || 'public',
fullSignature: `function ${func.name}(${inputs}) ${func.visibility || 'public'} ${stateMutability} ${outputs ? `returns (${outputs})` : ''}`
};
});
}
/**
* Extract event signatures from contract ABI
* @param {Array} abi - Contract ABI
* @returns {Array} Event signatures
*/
function extractEventSignatures(abi) {
if (!abi) return [];
return abi
.filter(item => item.type === 'event')
.map(event => {
const params = event.inputs?.map(input => {
return `${input.type} ${input.indexed ? 'indexed' : ''} ${input.name || ''}`;
}).join(', ') || '';
return {
name: event.name,
signature: `${event.name}(${params})`,
fullSignature: `event ${event.name}(${params})`
};
});
}
/**
* Format analysis results into a user-friendly string
* @param {Object} result - Analysis results
* @returns {string} Formatted results
*/
function formatAnalysisResults(result) {
let output = [];
output.push('=== 📊 CONTRACT ANALYSIS RESULTS ===');
output.push(`📍 Address: ${result.address}`);
output.push(`📜 Is Contract: ${result.isContract ? '✅ Yes' : '❌ No'}`);
if (!result.isContract) {
output.push(`💰 ETH Balance: ${result.ethBalance} ETH`);
output.push(`🔄 Transaction Count: ${result.transactionCount}`);
return output.join('\n');
}
output.push(`🔐 Is Verified: ${result.isVerified ? '✅ Yes' : '❌ No'}`);
if (result.contractCreator) {
output.push(`👤 Contract Creator: ${result.contractCreator}`);
output.push(`🧾 Creation Tx: ${result.creationTx}`);
output.push(`⏰ Creation Time: ${new Date(result.creationTimestamp * 1000).toLocaleString()}`);
}
if (result.isVerified) {
output.push(`📋 Contract Name: ${result.contractName || 'Unknown'}`);
if (result.standards) {
output.push('\n📑 CONTRACT STANDARDS:');
output.push(` ERC20: ${result.standards.isERC20 ? '✅ Yes' : '❌ No'}`);
output.push(` ERC721 (NFT): ${result.standards.isERC721 ? '✅ Yes' : '❌ No'}`);
output.push(` ERC1155 (Multi Token): ${result.standards.isERC1155 ? '✅ Yes' : '❌ No'}`);
}
if (result.abi) {
const functions = extractFunctionSignatures(result.abi);
const events = extractEventSignatures(result.abi);
output.push(`\n📝 FUNCTIONS (${functions.length}):`);
functions.slice(0, 5).forEach(func => {
output.push(` - ${func.name}(${func.stateMutability})`);
});
if (functions.length > 5) {
output.push(` ... and ${functions.length - 5} more functions`);
}
output.push(`\n🔔 EVENTS (${events.length}):`);
events.slice(0, 5).forEach(event => {
output.push(` - ${event.name}`);
});
if (events.length > 5) {
output.push(` ... and ${events.length - 5} more events`);
}
}
if (result.securityAnalysis && result.securityAnalysis.issuesFound) {
output.push('\n⚠️ SECURITY ISSUES:');
result.securityAnalysis.issues.forEach(issue => {
output.push(` [${issue.severity}] ${issue.issue}`);
output.push(` ${issue.description}`);
});
}
} else {
if (result.probableType) {
output.push(`🔍 Probable Contract Type: ${result.probableType}`);
}
output.push('\n❌ Contract is not verified on Etherscan. Limited analysis available.');
}
if (result.error) {
output.push(`\n⚠️ Info: ${result.error}`);
}
return output.join('\n');
}