transferERC20
Send ERC20 tokens to a recipient address using token contract and amount details. Specify network or chain ID for transactions across Ethereum networks.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| amount | Yes | The amount of tokens to transfer (can be decimal, e.g. '1.5') | |
| chainId | No | Optional. The chain ID to use. If provided with a named network and they don't match, the RPC's chain ID will be used. | |
| gasLimit | No | ||
| gasPrice | No | ||
| provider | No | Optional. Either a network name or custom RPC URL. Use getAllNetworks to see available networks and their details, or getNetwork to get info about a specific network. You can use any network name returned by these tools as a provider value. | |
| recipientAddress | Yes | The Ethereum address to receive the tokens | |
| tokenAddress | Yes | The address of the ERC20 token contract |
Implementation Reference
- src/tools/handlers/erc20.ts:171-226 (handler)The primary handler for the transferERC20 MCP tool. Handles input validation, parameter mapping for backward compatibility, executes the transfer via ethersService, retrieves token info for response formatting, and returns success/error messages.transferERC20: async (args: unknown) => { const schema = z.object({ contractAddress: contractAddressSchema.optional(), tokenAddress: tokenAddressSchema, // Deprecated recipientAddress: CommonSchemas.ethereumAddress.describe('Recipient address for the tokens'), amount: amountSchema, provider: providerSchema, chainId: chainIdSchema, gasLimit: CommonSchemas.gasLimit, gasPrice: CommonSchemas.gasPrice, }); try { // First validate with friendly errors const validatedParams = validateWithFriendlyErrors( schema, args, 'Transfer ERC20 Tokens' ); // Then map deprecated parameters for backward compatibility const mapped = mapParameters(validatedParams); // Ensure we have a contract address (from either new or old parameter name) const contractAddr = mapped.contractAddress || validatedParams.tokenAddress; if (!contractAddr) { throw new Error('Contract address is required. Please provide either contractAddress or tokenAddress.'); } // Create options object for transaction parameters const options: TokenOperationOptions = {}; if (validatedParams.gasLimit) options.gasLimit = validatedParams.gasLimit; if (validatedParams.gasPrice) options.gasPrice = validatedParams.gasPrice; const tx = await ethersService.transferERC20( contractAddr, validatedParams.recipientAddress, validatedParams.amount, mapped.provider, mapped.chainId, options ); // Get token info to format the response const tokenInfo = await ethersService.getERC20TokenInfo(contractAddr, mapped.provider, mapped.chainId); return { content: [{ type: "text", text: `Successfully transferred ${validatedParams.amount} ${tokenInfo.symbol} to ${validatedParams.recipientAddress}.\nTransaction Hash: ${tx.hash}` }] }; } catch (error) { return createErrorResponse(error, 'transferring tokens'); } },
- MCP tool schema definition for transferERC20, including name, description, and detailed inputSchema with properties, descriptions, and required fields.{ name: "transferERC20", description: "Transfer ERC20 tokens from the connected wallet to another address", inputSchema: { type: "object", properties: { tokenAddress: { type: "string", description: "The address of the ERC20 token contract" }, recipientAddress: { type: "string", description: "The Ethereum address to receive the tokens" }, amount: { type: "string", description: "The amount of tokens to transfer (can be decimal, e.g. '1.5')" }, provider: { type: "string", description: "Optional. Either a network name or custom RPC URL. Use getSupportedNetworks to get a list of supported networks." }, chainId: { type: "number", description: "Optional. The chain ID to use. If provided with a named network and they don't match, the RPC's chain ID will be used." }, gasLimit: { type: "string", description: "Optional. The gas limit for the transaction" }, gasPrice: { type: "string", description: "Optional. The gas price for the transaction in gwei" } }, required: ["tokenAddress", "recipientAddress", "amount"] }
- src/services/erc/erc20.ts:270-349 (helper)Core helper function implementing ERC20 transfer logic: rate limiting, caching, token decimals lookup, balance sufficiency check, contract interaction via ethers.js transfer method, transaction overrides, and cache invalidation.export async function transfer( ethersService: EthersService, tokenAddress: string, recipientAddress: string, amount: string, provider?: string, chainId?: number, options: TokenOperationOptions = {} ): Promise<ethers.TransactionResponse> { metrics.incrementCounter('erc20.transfer'); return timeAsync('erc20.transfer', async () => { try { // Check rate limiting for write operations const identity = `${tokenAddress}:transfer`; if (!rateLimiter.consume('transaction', identity)) { throw new ERC20Error('Rate limit exceeded for token transfers'); } // Get provider and signer from ethers service const ethersProvider = ethersService['getProvider'](provider, chainId); const signer = ethersService['getSigner'](provider, chainId); // Get token info for decimals const tokenInfo = await getTokenInfo(ethersService, tokenAddress, provider, chainId); // Parse input amount to wei equivalent based on token decimals const amountInWei = ethers.parseUnits(amount, tokenInfo.decimals); // Get current balance const contract = new ethers.Contract(tokenAddress, ERC20_ABI, ethersProvider); const walletAddress = await signer.getAddress(); const balance = await contract.balanceOf(walletAddress); // Check if balance is sufficient if (balance < amountInWei) { throw new InsufficientBalanceError( tokenAddress, ethers.formatUnits(amountInWei, tokenInfo.decimals), ethers.formatUnits(balance, tokenInfo.decimals) ); } // Create contract instance with signer const contractWithSigner = new ethers.Contract(tokenAddress, ERC20_ABI, signer); // Prepare transaction overrides const overrides: ethers.Overrides = {}; if (options.gasLimit) overrides.gasLimit = options.gasLimit; if (options.gasPrice) overrides.gasPrice = options.gasPrice; if (options.maxFeePerGas) overrides.maxFeePerGas = options.maxFeePerGas; if (options.maxPriorityFeePerGas) overrides.maxPriorityFeePerGas = options.maxPriorityFeePerGas; if (options.nonce !== undefined) overrides.nonce = options.nonce; // Send transaction const tx = await contractWithSigner.transfer(recipientAddress, amountInWei, overrides); // Invalidate balance caches const senderCacheKey = createTokenCacheKey( CACHE_KEYS.ERC20_BALANCE, tokenAddress, walletAddress, chainId ); const recipientCacheKey = createTokenCacheKey( CACHE_KEYS.ERC20_BALANCE, tokenAddress, recipientAddress, chainId ); balanceCache.delete(senderCacheKey); balanceCache.delete(recipientCacheKey); return tx; } catch (error) { logger.debug('Error transferring ERC20 tokens', { tokenAddress, recipientAddress, amount, error }); throw handleTokenError(error, 'Failed to transfer tokens'); } }); }