Skip to content

Instantly share code, notes, and snippets.

@shopglobal
Last active August 20, 2024 22:19
Show Gist options
  • Save shopglobal/0c0382d5c2e5f45480297d1a5b92c21e to your computer and use it in GitHub Desktop.
Save shopglobal/0c0382d5c2e5f45480297d1a5b92c21e to your computer and use it in GitHub Desktop.
ERC20 Token Factory

To create a token factory on Ethereum that deploys ownable ERC20 tokens and charges 0.02 ETH per deployment, you can follow these steps. The token factory will be a smart contract written in Solidity that deploys new instances of an ERC20 token contract. The deployer (caller of the deploy function) will be the owner of the new ERC20 token.

Step 1: Create the Ownable ERC20 Token Contract

First, create an ERC20 token contract that implements the Ownable pattern. This contract will be used by the token factory to deploy new tokens.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CustomERC20 is ERC20, Ownable {
    constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        transferOwnership(msg.sender);
    }
}

Step 2: Create the Token Factory Contract

Next, create the token factory contract that will deploy new instances of the CustomERC20 contract and charge a fee of 0.02 ETH per deployment.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./CustomERC20.sol";

contract TokenFactory {
    uint256 public constant DEPLOYMENT_FEE = 0.02 ether;
    address public owner;

    event TokenDeployed(address indexed tokenAddress, address indexed owner, string name, string symbol, uint256 initialSupply);

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the factory owner");
        _;
    }

    function deployToken(string memory name, string memory symbol, uint256 initialSupply) external payable returns (address) {
        require(msg.value >= DEPLOYMENT_FEE, "Insufficient ETH for deployment fee");

        // Transfer the deployment fee to the factory owner
        payable(owner).transfer(DEPLOYMENT_FEE);

        // Deploy the new token
        CustomERC20 newToken = new CustomERC20(name, symbol, initialSupply);
        newToken.transferOwnership(msg.sender);

        emit TokenDeployed(address(newToken), msg.sender, name, symbol, initialSupply);

        return address(newToken);
    }

    // Optional: Function to withdraw any excess ETH sent
    function withdrawExcessETH() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No excess ETH to withdraw");
        payable(owner).transfer(balance);
    }
}

Step 3: Deploy the Contracts

  1. Compile and Deploy the CustomERC20 Contract: Compile the CustomERC20 contract and deploy it on the Ethereum network.

  2. Deploy the TokenFactory Contract: After deploying the CustomERC20 contract, compile and deploy the TokenFactory contract. This contract will handle the deployment of new ERC20 tokens.

Step 4: Interact with the Token Factory

Once the TokenFactory contract is deployed, users can interact with it to create new ERC20 tokens:

  1. Deploy a New Token:

    • Users call the deployToken function of the TokenFactory contract.
    • They need to provide the name, symbol, and initial supply of the token, as well as send 0.02 ETH as the deployment fee.
  2. Token Ownership:

    • The TokenFactory automatically sets the caller of the deployToken function as the owner of the new ERC20 token.
    • The owner of the factory contract receives the 0.02 ETH fee.

Example of Deploying a Token

Here’s how a user might deploy a new token via the factory:

pragma solidity ^0.8.0;

import "./TokenFactory.sol";

contract DeployNewToken {
    function createMyToken(TokenFactory factory) external payable {
        address newToken = factory.deployToken{value: 0.02 ether}("MyToken", "MTK", 1000000 * 10 ** 18);
        // Now newToken is the address of the newly created token.
    }
}

Final Notes

  • Fee Handling: The factory contract ensures that exactly 0.02 ETH is paid for each deployment. Any excess ETH can be withdrawn by the factory owner using the withdrawExcessETH function.

  • Security Considerations: Ensure that your contracts are thoroughly tested and audited before deploying on the mainnet.

  • Customization: You can further customize the CustomERC20 contract to include features like minting, burning, or pausing tokens based on your

To automatically create a trading pair for the newly deployed ERC20 token on Uniswap, you can extend the TokenFactory contract to include this functionality. Here's how you can do it:

Step 1: Import Necessary Interfaces

You'll need to interact with Uniswap's UniswapV2Factory contract to create a new pair. Import the necessary Uniswap interfaces:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./CustomERC20.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";

contract TokenFactory {
    uint256 public constant DEPLOYMENT_FEE = 0.02 ether;
    address public owner;
    address public uniswapFactory;
    address public uniswapRouter;

    event TokenDeployed(
        address indexed tokenAddress,
        address indexed owner,
        string name,
        string symbol,
        uint256 initialSupply,
        address pairAddress
    );

    constructor(address _uniswapFactory, address _uniswapRouter) {
        owner = msg.sender;
        uniswapFactory = _uniswapFactory;
        uniswapRouter = _uniswapRouter;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the factory owner");
        _;
    }

    function deployToken(
        string memory name,
        string memory symbol,
        uint256 initialSupply,
        address baseToken // e.g., WETH
    ) external payable returns (address) {
        require(msg.value >= DEPLOYMENT_FEE, "Insufficient ETH for deployment fee");

        // Transfer the deployment fee to the factory owner
        payable(owner).transfer(DEPLOYMENT_FEE);

        // Deploy the new token
        CustomERC20 newToken = new CustomERC20(name, symbol, initialSupply);
        newToken.transferOwnership(msg.sender);

        // Create a Uniswap pair
        address pair = IUniswapV2Factory(uniswapFactory).createPair(address(newToken), baseToken);

        // Optionally: Provide initial liquidity, set allowance, etc.

        emit TokenDeployed(address(newToken), msg.sender, name, symbol, initialSupply, pair);

        return address(newToken);
    }

    // Optional: Function to withdraw any excess ETH sent
    function withdrawExcessETH() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No excess ETH to withdraw");
        payable(owner).transfer(balance);
    }
}

Step 2: Explanation of the Code

  • Uniswap Factory and Router Addresses:

    • The contract constructor now accepts the addresses of the Uniswap Factory (_uniswapFactory) and Uniswap Router (_uniswapRouter).
    • These addresses are stored in the contract's state and used when deploying tokens and creating pairs.
  • Uniswap Pair Creation:

    • The deployToken function now includes a step to automatically create a trading pair on Uniswap using the createPair function from IUniswapV2Factory.
    • The baseToken is an address of an existing token like WETH, USDT, or USDC that you want to pair with the new token.
  • Event Emission:

    • The TokenDeployed event now also includes the address of the Uniswap pair.

Step 3: Deploy and Test the Token Factory

  1. Deploy the TokenFactory Contract:

    • Deploy the TokenFactory contract, passing the Uniswap Factory and Router addresses as arguments.
  2. Deploy a New Token:

    • When you call the deployToken function, it will automatically deploy the token, create a Uniswap pair with the specified base token, and return the addresses of both the token and the pair.

Optional: Provide Initial Liquidity

You can extend the contract to provide initial liquidity automatically. This would involve interacting with the IUniswapV2Router02 interface to call the addLiquidity function after the pair is created.

Here's a conceptual example of adding liquidity:

function addLiquidity(
    address token,
    uint256 tokenAmount,
    uint256 ethAmount
) internal {
    IERC20(token).approve(uniswapRouter, tokenAmount);

    IUniswapV2Router02(uniswapRouter).addLiquidityETH{value: ethAmount}(
        token,
        tokenAmount,
        0, // slippage is unavoidable
        0, // slippage is unavoidable
        msg.sender,
        block.timestamp
    );
}

You would integrate this function within deployToken to automatically add liquidity to the Uniswap pair.

Final Considerations

  • Gas Costs: Deploying a new token and creating a Uniswap pair can be gas-intensive. Ensure that the DEPLOYMENT_FEE covers these costs adequately.
  • Security: Make sure the contracts are thoroughly tested and possibly audited before deploying them on the mainnet.

With these changes, the TokenFactory contract will not only deploy new ownable ERC20 tokens but also automatically create trading pairs on Uniswap, making the new token immediately tradable.

To provide more flexibility in your token factory for deploying tokens that can be either mintable, burnable, both, or neither, you can enhance the CustomERC20 contract and modify the TokenFactory to allow these options during deployment.

Step 1: Create the CustomERC20 Contract

Here's a version of the CustomERC20 contract that supports optional minting and burning:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";

contract CustomERC20 is ERC20, Ownable {
    bool public isMintable;
    bool public isBurnable;

    constructor(
        string memory name,
        string memory symbol,
        uint256 initialSupply,
        bool _isMintable,
        bool _isBurnable
    ) ERC20(name, symbol) {
        isMintable = _isMintable;
        isBurnable = _isBurnable;

        // Mint the initial supply to the deployer
        _mint(msg.sender, initialSupply);

        // Transfer ownership to the deployer
        transferOwnership(msg.sender);
    }

    function mint(address to, uint256 amount) external onlyOwner {
        require(isMintable, "Minting is disabled for this token");
        _mint(to, amount);
    }

    function burn(uint256 amount) public override {
        require(isBurnable, "Burning is disabled for this token");
        super.burn(amount);
    }
}

Step 2: Update the TokenFactory Contract

Now, we need to update the TokenFactory to allow the deployment of tokens with customizable mintable and burnable features.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./CustomERC20.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";

contract TokenFactory {
    uint256 public constant DEPLOYMENT_FEE = 0.02 ether;
    address public owner;
    address public uniswapFactory;

    event TokenDeployed(
        address indexed tokenAddress,
        address indexed owner,
        string name,
        string symbol,
        uint256 initialSupply,
        bool isMintable,
        bool isBurnable,
        address pairAddress
    );

    constructor(address _uniswapFactory) {
        owner = msg.sender;
        uniswapFactory = _uniswapFactory;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the factory owner");
        _;
    }

    function deployToken(
        string memory name,
        string memory symbol,
        uint256 initialSupply,
        bool isMintable,
        bool isBurnable,
        address baseToken // e.g., WETH
    ) external payable returns (address) {
        require(msg.value >= DEPLOYMENT_FEE, "Insufficient ETH for deployment fee");

        // Transfer the deployment fee to the factory owner
        payable(owner).transfer(DEPLOYMENT_FEE);

        // Deploy the new token
        CustomERC20 newToken = new CustomERC20(name, symbol, initialSupply, isMintable, isBurnable);
        newToken.transferOwnership(msg.sender);

        // Create a Uniswap pair
        address pair = IUniswapV2Factory(uniswapFactory).createPair(address(newToken), baseToken);

        emit TokenDeployed(
            address(newToken),
            msg.sender,
            name,
            symbol,
            initialSupply,
            isMintable,
            isBurnable,
            pair
        );

        return address(newToken);
    }

    // Optional: Function to withdraw any excess ETH sent
    function withdrawExcessETH() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No excess ETH to withdraw");
        payable(owner).transfer(balance);
    }
}

Step 3: Deploy and Test

  1. Deploy the CustomERC20 Contract:

    • This contract can now be used by the factory to deploy tokens with options for minting and burning.
  2. Deploy the TokenFactory Contract:

    • Deploy the TokenFactory contract, passing the Uniswap Factory address as an argument.
  3. Deploy a New Token:

    • Users can call the deployToken function with additional parameters for isMintable and isBurnable to specify whether the new token should support these features.
    • A Uniswap pair will be created automatically for the new token.

Example of Deploying a Token

Here’s how you might call the deployToken function to create a mintable and burnable token:

tokenFactory.deployToken{value: 0.02 ether}("MyToken", "MTK", 1000000 * 10 ** 18, true, true, WETH);

Final Notes

  • Customizability: The new version of the token contract allows users to decide whether their token can be minted or burned.
  • Security: Ensure that your contracts are thoroughly tested, especially when adding features like minting, which can affect the total supply of the token.

With these changes, the TokenFactory can deploy a wider variety of tokens, each tailored to the specific needs of the user, including options for minting and burning.

To create a React Native UI/UX with HTML and CSS that allows users to input a token's supply, name, symbol, and check options for isMintable and isBurnable, I'll guide you through setting up the basic structure, form, and interaction with the blockchain to deploy the token using the provided ABI.

Step 1: Set Up the Project

  1. Install React Native CLI: If you haven't already, you can install React Native CLI by running:

    npm install -g react-native-cli
  2. Create a New React Native Project:

    npx react-native init TokenFactoryApp
  3. Navigate to the Project Directory:

    cd TokenFactoryApp
  4. Install Required Dependencies: Install the necessary packages:

    npm install web3 ethers

Step 2: Create the Form UI

Inside the App.js file, we will create a form where users can input the token name, symbol, initial supply, and select the mintable and burnable options.

import React, { useState } from 'react';
import { StyleSheet, Text, TextInput, View, Button, CheckBox, Alert } from 'react-native';
import { ethers } from 'ethers';
import abi from './TokenFactoryABI.json'; // Make sure to include the ABI file

export default function App() {
  const [name, setName] = useState('');
  const [symbol, setSymbol] = useState('');
  const [supply, setSupply] = useState('');
  const [isMintable, setIsMintable] = useState(false);
  const [isBurnable, setIsBurnable] = useState(false);

  const deployToken = async () => {
    try {
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner();
      
      const contractAddress = "0xYourTokenFactoryAddress"; // Replace with your deployed factory contract address
      const contract = new ethers.Contract(contractAddress, abi, signer);

      const tx = await contract.deployToken(
        name,
        symbol,
        ethers.utils.parseUnits(supply, 18),
        isMintable,
        isBurnable,
        "0xYourBaseTokenAddress", // Replace with your base token address like WETH
        { value: ethers.utils.parseEther("0.02") }
      );
      
      await tx.wait();
      Alert.alert('Token Deployed', `Transaction Hash: ${tx.hash}`);
    } catch (error) {
      Alert.alert('Error', error.message);
    }
  };

  return (
    <View style={styles.container}>
      <Text style={styles.header}>Deploy Your ERC20 Token</Text>
      
      <TextInput
        style={styles.input}
        placeholder="Token Name"
        value={name}
        onChangeText={setName}
      />

      <TextInput
        style={styles.input}
        placeholder="Token Symbol"
        value={symbol}
        onChangeText={setSymbol}
      />

      <TextInput
        style={styles.input}
        placeholder="Initial Supply"
        value={supply}
        keyboardType="numeric"
        onChangeText={setSupply}
      />

      <View style={styles.checkboxContainer}>
        <CheckBox
          value={isMintable}
          onValueChange={setIsMintable}
        />
        <Text style={styles.label}>Mintable</Text>
      </View>

      <View style={styles.checkboxContainer}>
        <CheckBox
          value={isBurnable}
          onValueChange={setIsBurnable}
        />
        <Text style={styles.label}>Burnable</Text>
      </View>

      <Button title="Deploy Token" onPress={deployToken} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    padding: 16,
    backgroundColor: '#f8f9fa',
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 24,
    textAlign: 'center',
  },
  input: {
    height: 40,
    borderColor: '#ccc',
    borderWidth: 1,
    marginBottom: 12,
    paddingLeft: 8,
    borderRadius: 4,
    backgroundColor: '#fff',
  },
  checkboxContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    marginBottom: 12,
  },
  label: {
    marginLeft: 8,
  },
});

Step 3: ABI File and Deployment

  1. Create the ABI File:

    • Save your contract ABI in a JSON file, e.g., TokenFactoryABI.json, and place it in your project directory.
  2. Replace the Placeholder Addresses:

    • Make sure to replace "0xYourTokenFactoryAddress" with the actual address of your deployed TokenFactory contract.
    • Replace "0xYourBaseTokenAddress" with the base token address (e.g., WETH) that you want to pair with.
  3. Running the Application:

    • If you're running on an Android emulator, use:
      npx react-native run-android
    • For an iOS simulator, use:
      npx react-native run-ios

Step 4: Connecting with MetaMask (Web3Provider)

This example assumes the user is accessing the DApp via a mobile browser with MetaMask installed. The Web3Provider from ethers.js handles interactions with the blockchain.

Final Notes

  • Testing: Before deploying on the mainnet, test thoroughly on a testnet like Ropsten or Rinkeby.
  • UX Enhancements: You can add better error handling, loading states, and further customize the UI to improve user experience.
  • Security: Always consider security best practices when interacting with smart contracts and handling user inputs.

This setup provides a basic UI/UX for deploying customizable ERC20 tokens using React Native, allowing users to input their desired token parameters and deploy them directly to the blockchain.

The ABI (Application Binary Interface) is a JSON object that describes how to interact with a smart contract on the Ethereum blockchain (or other EVM-compatible chains). It includes information about the contract's functions, their inputs, outputs, and events.

For the TokenFactory contract we discussed earlier, the ABI would include details about the deployToken function, which allows the user to deploy new tokens. Below is an example of what the ABI might look like for that contract:

Example ABI for TokenFactory

This ABI corresponds to the TokenFactory contract that deploys ownable ERC20 tokens with options for mintable and burnable features.

[
    {
        "inputs": [
            {
                "internalType": "string",
                "name": "name",
                "type": "string"
            },
            {
                "internalType": "string",
                "name": "symbol",
                "type": "string"
            },
            {
                "internalType": "uint256",
                "name": "initialSupply",
                "type": "uint256"
            },
            {
                "internalType": "bool",
                "name": "isMintable",
                "type": "bool"
            },
            {
                "internalType": "bool",
                "name": "isBurnable",
                "type": "bool"
            },
            {
                "internalType": "address",
                "name": "baseToken",
                "type": "address"
            }
        ],
        "name": "deployToken",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "payable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "DEPLOYMENT_FEE",
        "outputs": [
            {
                "internalType": "uint256",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "owner",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "uniswapFactory",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "internalType": "address",
                "name": "_uniswapFactory",
                "type": "address"
            }
        ],
        "name": "constructor",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "withdrawExcessETH",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "transferOwnership",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "anonymous": false,
        "inputs": [
            {
                "indexed": true,
                "internalType": "address",
                "name": "tokenAddress",
                "type": "address"
            },
            {
                "indexed": true,
                "internalType": "address",
                "name": "owner",
                "type": "address"
            },
            {
                "indexed": false,
                "internalType": "string",
                "name": "name",
                "type": "string"
            },
            {
                "indexed": false,
                "internalType": "string",
                "name": "symbol",
                "type": "string"
            },
            {
                "indexed": false,
                "internalType": "uint256",
                "name": "initialSupply",
                "type": "uint256"
            },
            {
                "indexed": false,
                "internalType": "bool",
                "name": "isMintable",
                "type": "bool"
            },
            {
                "indexed": false,
                "internalType": "bool",
                "name": "isBurnable",
                "type": "bool"
            },
            {
                "indexed": false,
                "internalType": "address",
                "name": "pairAddress",
                "type": "address"
            }
        ],
        "name": "TokenDeployed",
        "type": "event"
    }
]

Key Elements of the ABI

  1. Functions:

    • deployToken: The main function to deploy a new token. It accepts parameters like name, symbol, initialSupply, isMintable, isBurnable, and baseToken. The function is marked as payable, meaning it can accept Ether, which is required for the deployment fee.

    • DEPLOYMENT_FEE, owner, uniswapFactory: These are view functions that return information about the contract's deployment fee, owner, and the Uniswap factory address.

    • withdrawExcessETH: A function to withdraw any excess ETH that might be in the contract.

    • transferOwnership: A function to transfer ownership of the contract.

  2. Constructor:

    • constructor: Initializes the contract with the Uniswap factory address.
  3. Events:

    • TokenDeployed: This event is emitted when a new token is deployed. It includes details such as the token's address, owner, name, symbol, initial supply, and whether it is mintable or burnable.

Using the ABI

In your React Native app, you’ll use this ABI along with the contract address to interact with the TokenFactory smart contract. The ABI is crucial for encoding the data required to call the contract's functions and for decoding the responses and events emitted by the contract.

Here’s how you would use the ABI in your React Native app:

import abi from './TokenFactoryABI.json';

// Example: Instantiate the contract using ethers.js
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contract = new ethers.Contract('0xYourTokenFactoryAddress', abi, signer);

Replace '0xYourTokenFactoryAddress' with your deployed contract's address and ensure you have the correct ABI file to interact with the contract’s functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment