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
- Network: Base Mainnet (Chain ID: 8453)
- RPC URL: https://mainnet.base.org (opens in a new tab)
- Factory Contract:
0xeF6ce237F69D238Fe3CfAB801f24b4cfd3A54B9D - Token Implementation:
0x974d1b4dA02ee570041175db77e373522F8e6AB0
Block Explorer
- Factory Contract (opens in a new tab) (Verified)
How It Works
The createToken function executes everything in one transaction:
- ✓ Creates your ERC20 token (using minimal proxy pattern for gas efficiency)
- ✓ Initializes a Uniswap V4 pool (ETH/Token pair)
- ✓ Adds liquidity to the pool
- ✓ Transfers the LP NFT to the token contract
- ✓ Sends remaining tokens to you (the creator)
- ✓ 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
feeReceiveraddress
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)
| Parameter | Type | Description | Example |
|---|---|---|---|
name | string | Token name | "My Awesome Token" |
symbol | string | Token ticker (3-5 uppercase letters) | "MAT" |
totalSupply | uint256 | Total token supply in wei (18 decimals) | 1000000000000000000000000 (1M tokens) |
tokenAmountForLiquidity | uint256 | Tokens to add to pool in wei | 500000000000000000000000 (500K tokens) |
feeReceiver | address | Who receives token fees from sells | 0x1ab1... |
msg.value | ETH | ETH to add to liquidity pool | 0.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 =
1000000000000000000000000wei
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 ethersBasic 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
-
Clean, single-column form
- Clear labels and helper text
- Input validation with error messages
- Live preview of token distribution
-
Liquidity Slider
- Visual slider for liquidity percentage
- Show split: "X tokens to pool, Y tokens to you"
- Recommend 50% as default
-
Summary Section
- Show pool fee (1.00%)
- Explain fee split (who gets what)
- Display estimated gas cost
- Total cost = ETH for liquidity + gas fees
-
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
-
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:
| Error | Cause | Solution |
|---|---|---|
| "Must provide token amount" | liquidityTokens = 0 | Set liquidityTokens > 0 |
| "Token amount exceeds supply" | liquidityTokens > totalSupply | Reduce liquidity amount |
| "Invalid fee receiver" | feeReceiver = address(0) | Use valid address |
| "Insufficient funds" | Not enough ETH | Check balance + gas |
| "User rejected transaction" | User cancelled in MetaMask | Allow retry |
| "Wrong network" | Not on Base | Prompt to switch networks |
Dependencies
Core Dependencies
npm install ethersOptional Wallet Libraries
# wagmi + RainbowKit (React)
npm install wagmi @rainbow-me/rainbowkit
# web3modal (Multi-framework)
npm install @web3modal/wagmi
# ConnectKit (React)
npm install connectkitSecurity 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
- Test thoroughly on Base mainnet with small amounts
- Implement proper error handling for all edge cases
- Show transaction progress and status
- Provide clear feedback at each step
- Validate inputs before submitting transactions
- Cache gas estimates to show users expected costs
For Users
- Start small - Test with minimal ETH amounts first
- Verify addresses - Double-check fee receiver address
- Understand fees - Know who gets what
- Check balance - Ensure you have enough ETH for gas + liquidity
- Save token address - Copy and save your token address immediately
- Add to wallet - Import your new token to MetaMask
Support
For issues or questions:
- View contracts on Basescan (opens in a new tab)
- Check Base documentation (opens in a new tab)
- Review Uniswap V4 docs (opens in a new tab)
- Inspect transaction logs for debugging