Smart Contract Integration
Token Creation & Deployment

Token Creation & Deployment

Build a token deployment interface that allows users to create their own ERC20 tokens with automatic Uniswap V4 liquidity pools on Base Mainnet.

Overview

The Token Factory enables users to deploy fully functional ERC20 tokens with:

  • Automatic pool creation on Uniswap V4
  • Instant liquidity provision
  • Decentralized ownership (automatically renounced)
  • Built-in fee distribution
  • Gas-efficient deployment (minimal proxy pattern)

Contract Details

Block Explorer

How It Works

The createToken function executes everything in one transaction:

  1. ✓ Creates your ERC20 token (using minimal proxy pattern for gas efficiency)
  2. ✓ Initializes a Uniswap V4 pool (ETH/Token pair)
  3. ✓ Adds liquidity to the pool
  4. ✓ Transfers the LP NFT to the token contract
  5. ✓ Sends remaining tokens to you (the creator)
  6. ✓ Renounces ownership (token becomes truly decentralized)

Pool Configuration

  • Trading Fee: 1.00% (10000 basis points)
  • Tick Spacing: 200
  • Currency0: ETH (native)
  • Currency1: Your Token
  • Hooks: None (address(0))

Fee Distribution

  • ETH fees (from buys) → Factory Creator (0x3B1601C4e82B19beb3886A6A74b3C89Ef28bD67F)
  • Token fees (from sells) → Your specified feeReceiver address

Factory Contract ABI

[
  {
    "inputs": [
      {"type": "string", "name": "name"},
      {"type": "string", "name": "symbol"},
      {"type": "uint256", "name": "totalSupply"},
      {"type": "uint256", "name": "tokenAmountForLiquidity"},
      {"type": "address", "name": "feeReceiver"}
    ],
    "name": "createToken",
    "outputs": [
      {"type": "address", "name": "tokenAddress"},
      {"type": "uint256", "name": "tokenId"}
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "factoryCreator",
    "outputs": [{"type": "address"}],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_FEE",
    "outputs": [{"type": "uint24"}],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "DEFAULT_TICK_SPACING",
    "outputs": [{"type": "int24"}],
    "stateMutability": "view",
    "type": "function"
  }
]

Function Parameters

createToken(name, symbol, totalSupply, tokenAmountForLiquidity, feeReceiver)

ParameterTypeDescriptionExample
namestringToken name"My Awesome Token"
symbolstringToken ticker (3-5 uppercase letters)"MAT"
totalSupplyuint256Total token supply in wei (18 decimals)1000000000000000000000000 (1M tokens)
tokenAmountForLiquidityuint256Tokens to add to pool in wei500000000000000000000000 (500K tokens)
feeReceiveraddressWho receives token fees from sells0x1ab1...
msg.valueETHETH to add to liquidity pool0.1 ETH

Parameter Details

name (string)

  • Token full name
  • Max length: Reasonable (gas cost increases with length)
  • Example: "My Awesome Token"

symbol (string)

  • Token ticker symbol
  • Convention: 3-5 uppercase letters
  • Example: "MAT"

totalSupply (uint256)

  • Total token supply in wei (18 decimals)
  • Must be > 0
  • Tip: Use ethers.parseEther("1000000") for 1M tokens
  • Example: 1,000,000 tokens = 1000000000000000000000000 wei

tokenAmountForLiquidity (uint256)

  • How many tokens to add to the pool (in wei)
  • Must be > 0 and ≤ totalSupply
  • Remaining tokens are sent to the creator
  • Example: 500,000 tokens (50% of supply)

feeReceiver (address)

  • Who receives token fees from sells
  • Must be a valid address (not address(0))
  • Tip: Use creator's wallet or a dedicated fee collector

msg.value (ETH)

  • Amount of ETH to add to the liquidity pool
  • Can be 0 for token-only pools
  • More ETH = higher initial token price
  • Example: 0.1 ETH, 1 ETH, etc.

Implementation Guide

Installation

npm install ethers

Basic Implementation

import { ethers } from 'ethers';
 
const FACTORY_ADDRESS = '0xeF6ce237F69D238Fe3CfAB801f24b4cfd3A54B9D';
 
const FACTORY_ABI = [
  'function createToken(string name, string symbol, uint256 totalSupply, uint256 tokenAmountForLiquidity, address feeReceiver) payable returns (address tokenAddress, uint256 tokenId)',
  'function factoryCreator() view returns (address)',
  'function DEFAULT_FEE() view returns (uint24)',
  'function DEFAULT_TICK_SPACING() view returns (int24)'
];
 
async function createToken(params) {
  const { name, symbol, totalSupply, liquidityTokens, feeReceiver, ethAmount } = params;
  
  // Connect wallet
  if (!window.ethereum) {
    throw new Error('No wallet found. Please install MetaMask.');
  }
  
  const provider = new ethers.BrowserProvider(window.ethereum);
  await provider.send('eth_requestAccounts', []);
  const signer = await provider.getSigner();
  
  // Connect to factory
  const factory = new ethers.Contract(FACTORY_ADDRESS, FACTORY_ABI, signer);
  
  // Convert to wei (18 decimals)
  const totalSupplyWei = ethers.parseEther(totalSupply.toString());
  const liquidityTokensWei = ethers.parseEther(liquidityTokens.toString());
  const ethAmountWei = ethers.parseEther(ethAmount.toString());
  
  try {
    // Call createToken
    const tx = await factory.createToken(
      name,
      symbol,
      totalSupplyWei,
      liquidityTokensWei,
      feeReceiver,
      { value: ethAmountWei }
    );
    
    console.log('Transaction sent:', tx.hash);
    console.log('Waiting for confirmation...');
    
    const receipt = await tx.wait();
    console.log('Token created!', receipt);
    
    // Parse the TokenCreated event from logs
    const event = receipt.logs.find(log => {
      try {
        return factory.interface.parseLog(log)?.name === 'TokenCreated';
      } catch {
        return false;
      }
    });
    
    if (event) {
      const parsed = factory.interface.parseLog(event);
      return {
        success: true,
        txHash: receipt.hash,
        tokenAddress: parsed.args.token,
        lpTokenId: parsed.args.lpTokenId.toString(),
        creator: parsed.args.creator,
        feeReceiver: parsed.args.feeReceiver
      };
    }
    
    return {
      success: true,
      txHash: receipt.hash
    };
  } catch (error) {
    console.error('Error creating token:', error);
    return {
      success: false,
      error: error.message
    };
  }
}
 
// Example usage
const result = await createToken({
  name: 'My Awesome Token',
  symbol: 'MAT',
  totalSupply: 1000000,        // 1 million tokens
  liquidityTokens: 500000,     // 500k tokens to pool (50%)
  feeReceiver: '0x1ab186D064171DB900fa6701A3eF5C5153ecbC24',
  ethAmount: 0.1               // 0.1 ETH to pool
});
 
if (result.success) {
  console.log('Token Address:', result.tokenAddress);
  console.log('LP Token ID:', result.lpTokenId);
  console.log('View on Basescan:', `https://basescan.org/address/${result.tokenAddress}`);
}

React Component Example

Complete token creation form:

import React, { useState } from 'react';
import { ethers } from 'ethers';
 
export default function TokenCreator() {
  const [formData, setFormData] = useState({
    name: '',
    symbol: '',
    totalSupply: '1000000',
    liquidityPercent: 50,
    ethAmount: '0.1',
    feeReceiver: ''
  });
  
  const [isCreating, setIsCreating] = useState(false);
  const [result, setResult] = useState(null);
  const [error, setError] = useState(null);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setIsCreating(true);
    setError(null);
    setResult(null);
    
    try {
      const liquidityTokens = (formData.totalSupply * formData.liquidityPercent) / 100;
      
      const result = await createToken({
        name: formData.name,
        symbol: formData.symbol,
        totalSupply: formData.totalSupply,
        liquidityTokens: liquidityTokens,
        feeReceiver: formData.feeReceiver,
        ethAmount: formData.ethAmount
      });
      
      if (result.success) {
        setResult(result);
      } else {
        setError(result.error);
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setIsCreating(false);
    }
  };
  
  return (
    <div className="token-creator">
      <h1>Create Your Token</h1>
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>Token Name</label>
          <input
            type="text"
            value={formData.name}
            onChange={(e) => setFormData({...formData, name: e.target.value})}
            placeholder="My Awesome Token"
            required
          />
        </div>
        
        <div className="form-group">
          <label>Token Symbol</label>
          <input
            type="text"
            value={formData.symbol}
            onChange={(e) => setFormData({...formData, symbol: e.target.value.toUpperCase()})}
            placeholder="MAT"
            maxLength={10}
            required
          />
        </div>
        
        <div className="form-group">
          <label>Total Supply</label>
          <input
            type="number"
            value={formData.totalSupply}
            onChange={(e) => setFormData({...formData, totalSupply: e.target.value})}
            placeholder="1000000"
            min="1"
            required
          />
          <small>Total number of tokens to create</small>
        </div>
        
        <div className="form-group">
          <label>Liquidity % (Tokens to Pool)</label>
          <input
            type="range"
            value={formData.liquidityPercent}
            onChange={(e) => setFormData({...formData, liquidityPercent: e.target.value})}
            min="10"
            max="100"
            step="5"
          />
          <span>{formData.liquidityPercent}%</span>
          <small>
            Pool: {(formData.totalSupply * formData.liquidityPercent / 100).toLocaleString()} tokens
            <br/>
            You get: {(formData.totalSupply * (100 - formData.liquidityPercent) / 100).toLocaleString()} tokens
          </small>
        </div>
        
        <div className="form-group">
          <label>ETH Amount for Liquidity</label>
          <input
            type="number"
            value={formData.ethAmount}
            onChange={(e) => setFormData({...formData, ethAmount: e.target.value})}
            placeholder="0.1"
            min="0"
            step="0.01"
          />
          <small>ETH to pair with tokens in the pool (can be 0)</small>
        </div>
        
        <div className="form-group">
          <label>Fee Receiver Address</label>
          <input
            type="text"
            value={formData.feeReceiver}
            onChange={(e) => setFormData({...formData, feeReceiver: e.target.value})}
            placeholder="0x..."
            required
          />
          <small>Address that receives token fees from sells</small>
        </div>
        
        <div className="summary">
          <h3>Summary</h3>
          <p>Pool Fee: 1.00% (Uniswap V4)</p>
          <p>Token fees (sells) → Your fee receiver</p>
          <p>ETH fees (buys) → Factory creator</p>
          <p>Estimated Gas: ~$5-10 (Base mainnet)</p>
        </div>
        
        <button type="submit" disabled={isCreating}>
          {isCreating ? 'Creating Token...' : 'Create Token'}
        </button>
      </form>
      
      {error && (
        <div className="error">
          <h3>Error</h3>
          <p>{error}</p>
        </div>
      )}
      
      {result && (
        <div className="success">
          <h3>Token Created Successfully! 🎉</h3>
          <p><strong>Token Address:</strong> {result.tokenAddress}</p>
          <p><strong>LP Token ID:</strong> {result.lpTokenId}</p>
          <p><strong>Transaction:</strong> <a href={\`https://basescan.org/tx/\${result.txHash}\`} target="_blank">View on Basescan</a></p>
          <p><strong>Token Contract:</strong> <a href={\`https://basescan.org/address/\${result.tokenAddress}\`} target="_blank">View Token</a></p>
        </div>
      )}
    </div>
  );
}

Input Validation

Validate all inputs before submitting the transaction:

Token Name

  • ✓ Not empty
  • ✓ Reasonable length (1-50 characters)

Token Symbol

  • ✓ Not empty
  • ✓ 1-10 characters (typically 3-5)
  • ✓ Uppercase recommended

Total Supply

  • ✓ Greater than 0
  • ✓ Not absurdly large (prevent overflow)
  • ✓ Recommended: 1,000 to 1,000,000,000 tokens

Liquidity Tokens

  • ✓ Greater than 0
  • ✓ Less than or equal to total supply
  • ✓ Recommended: 30-80% of total supply

Fee Receiver

  • ✓ Valid Ethereum address (starts with 0x, 42 characters)
  • ✓ Not address(0)
  • ✓ Use ethers.isAddress() to validate

ETH Amount

  • ✓ Can be 0 (token-only pool)
  • ✓ User must have enough ETH balance
  • ✓ Check balance: await signer.getBalance()

Validation Example

function validateInputs(formData) {
  const errors = [];
  
  // Validate name
  if (!formData.name || formData.name.length < 1 || formData.name.length > 50) {
    errors.push('Token name must be 1-50 characters');
  }
  
  // Validate symbol
  if (!formData.symbol || formData.symbol.length < 1 || formData.symbol.length > 10) {
    errors.push('Token symbol must be 1-10 characters');
  }
  
  // Validate total supply
  const supply = parseFloat(formData.totalSupply);
  if (isNaN(supply) || supply <= 0) {
    errors.push('Total supply must be greater than 0');
  }
  
  // Validate liquidity percentage
  const liquidityTokens = (supply * formData.liquidityPercent) / 100;
  if (liquidityTokens <= 0 || liquidityTokens > supply) {
    errors.push('Invalid liquidity amount');
  }
  
  // Validate fee receiver
  if (!ethers.isAddress(formData.feeReceiver)) {
    errors.push('Invalid fee receiver address');
  }
  
  // Validate ETH amount
  const ethAmount = parseFloat(formData.ethAmount);
  if (isNaN(ethAmount) || ethAmount < 0) {
    errors.push('Invalid ETH amount');
  }
  
  return errors;
}

UI/UX Requirements

Form Layout

  1. Clean, single-column form

    • Clear labels and helper text
    • Input validation with error messages
    • Live preview of token distribution
  2. Liquidity Slider

    • Visual slider for liquidity percentage
    • Show split: "X tokens to pool, Y tokens to you"
    • Recommend 50% as default
  3. Summary Section

    • Show pool fee (1.00%)
    • Explain fee split (who gets what)
    • Display estimated gas cost
    • Total cost = ETH for liquidity + gas fees
  4. Transaction States

    • Initial: Form ready
    • Pending: "Creating token..." with spinner
    • Success: Show token address, LP ID, Basescan links
    • Error: Clear error message with retry option
  5. Additional Features

    • "Use my address" button for fee receiver
    • Preset templates (meme coin, utility token, etc.)
    • Gas estimation before submission
    • Network check (must be Base)
    • Balance check (must have enough ETH)

Important Notes

Token Ownership

  • ✓ Ownership is automatically renounced after creation
  • ✓ Token becomes fully decentralized
  • No one can mint more tokens or change parameters

Liquidity Provision

  • ✓ The LP NFT is transferred to the token contract
  • ✓ Token contract can claim LP fees via get_rewards()
  • ✓ You don't need to manage the LP position

Fee Structure

  • ✓ 1% trading fee on all swaps
  • ✓ ETH fees (from buys) → Factory creator
  • ✓ Token fees (from sells) → Your fee receiver
  • ✓ Anyone can trigger fee distribution

Gas Costs

  • ✓ Typical: ~0.001-0.003 ETH on Base
  • ✓ Factory uses minimal proxy pattern (saves ~750k gas!)
  • ✓ One transaction does everything

Pool Parameters

  • ✓ Starting price determined by ETH/Token ratio
  • ✓ Wide tick range (supports full price spectrum)
  • Cannot be changed after creation

Testing

Test Parameters

{
  name: "Test Token",
  symbol: "TEST",
  totalSupply: 1000000,        // 1M tokens
  liquidityTokens: 500000,     // 500K tokens (50%)
  ethAmount: 0.01,             // 0.01 ETH
  feeReceiver: "0x..." // Your test wallet
}

Expected Result

  • ✓ Token created in ~5-15 seconds
  • ✓ Receive token address starting with 0x
  • ✓ You get 500,000 tokens in your wallet
  • ✓ Pool has 500,000 tokens + 0.01 ETH

Error Handling

Common errors and solutions:

ErrorCauseSolution
"Must provide token amount"liquidityTokens = 0Set liquidityTokens > 0
"Token amount exceeds supply"liquidityTokens > totalSupplyReduce liquidity amount
"Invalid fee receiver"feeReceiver = address(0)Use valid address
"Insufficient funds"Not enough ETHCheck balance + gas
"User rejected transaction"User cancelled in MetaMaskAllow retry
"Wrong network"Not on BasePrompt to switch networks

Dependencies

Core Dependencies

npm install ethers

Optional Wallet Libraries

# wagmi + RainbowKit (React)
npm install wagmi @rainbow-me/rainbowkit
 
# web3modal (Multi-framework)
npm install @web3modal/wagmi
 
# ConnectKit (React)
npm install connectkit

Security Checklist

Before launching:

  • ✓ Validate all inputs before submission
  • ✓ Check user is on Base network (chain ID 8453)
  • ✓ Verify user has sufficient ETH balance
  • ✓ Validate fee receiver is a real address
  • ✓ Show clear confirmation before transaction
  • ✓ Display all costs upfront (ETH + gas)
  • ✓ Handle rejected transactions gracefully
  • ✓ Never store private keys
  • ✓ Use HTTPS only

Best Practices

For Developers

  1. Test thoroughly on Base mainnet with small amounts
  2. Implement proper error handling for all edge cases
  3. Show transaction progress and status
  4. Provide clear feedback at each step
  5. Validate inputs before submitting transactions
  6. Cache gas estimates to show users expected costs

For Users

  1. Start small - Test with minimal ETH amounts first
  2. Verify addresses - Double-check fee receiver address
  3. Understand fees - Know who gets what
  4. Check balance - Ensure you have enough ETH for gas + liquidity
  5. Save token address - Copy and save your token address immediately
  6. Add to wallet - Import your new token to MetaMask

Support

For issues or questions: