Back to Tutorials
Intermediate40 min

Creating ERC-20 Tokens

Build and deploy your own fungible token

Creating ERC-20 Tokens

In this tutorial, you'll create and deploy your own ERC-20 token on Beatoz.

What is ERC-20?

ERC-20 is a standard for fungible tokens. All ERC-20 tokens have the same interface, making them compatible with wallets, exchanges, and other contracts.

Project Setup

mkdir my-token
cd my-token
npm init -y
npm install @beatoz/web3 @openzeppelin/contracts hardhat dotenv
npm install --save-dev @nomicfoundation/hardhat-toolbox
npx hardhat init

Writing the Token Contract

Testnet

Create contracts/MyToken.sol:

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

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

contract MyToken is ERC20, Ownable {
    uint8 private _decimals;
    
    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals_,
        uint256 initialSupply
    ) ERC20(name, symbol) Ownable(msg.sender) {
        _decimals = decimals_;
        _mint(msg.sender, initialSupply * 10 ** decimals_);
    }
    
    function decimals() public view virtual override returns (uint8) {
        return _decimals;
    }
    
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
    
    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
}

Compile the Contract

Update hardhat.config.js:

require("@nomicfoundation/hardhat-toolbox");

module.exports = {
  solidity: "0.8.19",
};
npx hardhat compile

Deploy Script

Create scripts/deploy-token.js:

const { Web3, TrxProtoBuilder } = require('@beatoz/web3');
const fs = require('fs');
const path = require('path');
require('dotenv').config();

async function main() {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const deployer = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  console.log('Deploying with:', deployer.address);
  
  // Load artifact
  const artifact = JSON.parse(
    fs.readFileSync(
      path.join(__dirname, '../artifacts/contracts/MyToken.sol/MyToken.json'),
      'utf-8'
    )
  );
  
  // Token parameters
  const tokenName = 'My Awesome Token';
  const tokenSymbol = 'MAT';
  const decimals = 18;
  const initialSupply = 1000000; // 1 million tokens
  
  // Create contract instance
  const contract = new web3.beatoz.Contract(artifact.abi);
  
  // Encode constructor
  const deployData = contract.deploy({
    data: artifact.bytecode,
    arguments: [tokenName, tokenSymbol, decimals, initialSupply],
  }).encodeABI();
  
  // Get account info
  const account = await web3.beatoz.getAccount(deployer.address);
  const status = await web3.beatoz.status();
  
  // Build transaction
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: deployer.address,
    nonce: account.nonce + 1,
    gas: 5000000,
    gasPrice: web3.utils.toFons('0.000001'),
    data: deployData,
    chainId: status.node_info.network,
  });
  
  // Sign and send
  const signedTx = deployer.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  if (result.deliver_tx.code !== 0) {
    throw new Error(`Deployment failed: ${result.deliver_tx.log}`);
  }
  
  console.log('✅ Token deployed successfully!');
  console.log('Token Name:', tokenName);
  console.log('Token Symbol:', tokenSymbol);
  console.log('Initial Supply:', initialSupply.toLocaleString());
  console.log('Transaction:', result.hash);
}

main().catch(console.error);

Token Interactions

Check Balance

async function getBalance(tokenAddress, ownerAddress) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const token = new web3.beatoz.Contract(ERC20_ABI, tokenAddress);
  
  const balance = await token.methods.balanceOf(ownerAddress).call();
  const decimals = await token.methods.decimals().call();
  
  const formatted = parseFloat(balance) / Math.pow(10, decimals);
  console.log(`Balance: ${formatted.toLocaleString()} tokens`);
  
  return formatted;
}

Transfer Tokens

async function transfer(tokenAddress, to, amount) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  
  const sender = web3.beatoz.accounts.privateKeyToAccount(
    process.env.PRIVATE_KEY
  );
  
  const token = new web3.beatoz.Contract(ERC20_ABI, tokenAddress);
  const decimals = await token.methods.decimals().call();
  
  // Convert to smallest unit
  const rawAmount = BigInt(amount * Math.pow(10, parseInt(decimals)));
  
  // Encode transfer call
  const data = token.methods.transfer(to, rawAmount.toString()).encodeABI();
  
  // Build and send transaction
  const accountInfo = await web3.beatoz.getAccount(sender.address);
  const status = await web3.beatoz.status();
  
  const tx = TrxProtoBuilder.buildContractTrxProto({
    from: sender.address,
    to: tokenAddress,
    nonce: accountInfo.nonce + 1,
    gas: 100000,
    gasPrice: web3.utils.toFons('0.000001'),
    data: data,
    chainId: status.node_info.network,
  });
  
  const signedTx = sender.signTransaction(tx);
  const result = await web3.beatoz.broadcastTxCommit(signedTx);
  
  console.log('Transfer complete! TX:', result.hash);
}

Approve Spending

async function approve(tokenAddress, spender, amount) {
  const token = new web3.beatoz.Contract(ERC20_ABI, tokenAddress);
  
  const data = token.methods.approve(
    spender,
    web3.utils.toWei(amount.toString(), 'ether')
  ).encodeABI();
  
  // ... build and send transaction
}

Check Allowance

async function getAllowance(tokenAddress, owner, spender) {
  const token = new web3.beatoz.Contract(ERC20_ABI, tokenAddress);
  
  const allowance = await token.methods.allowance(owner, spender).call();
  console.log('Allowance:', web3.utils.fromWei(allowance, 'ether'));
  
  return allowance;
}

ERC-20 Standard ABI

const ERC20_ABI = [
  {
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [{"type": "string"}],
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "symbol",
    "outputs": [{"type": "string"}],
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "decimals",
    "outputs": [{"type": "uint8"}],
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "totalSupply",
    "outputs": [{"type": "uint256"}],
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [{"name": "owner", "type": "address"}],
    "name": "balanceOf",
    "outputs": [{"type": "uint256"}],
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {"name": "to", "type": "address"},
      {"name": "amount", "type": "uint256"}
    ],
    "name": "transfer",
    "outputs": [{"type": "bool"}],
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {"name": "spender", "type": "address"},
      {"name": "amount", "type": "uint256"}
    ],
    "name": "approve",
    "outputs": [{"type": "bool"}],
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [
      {"name": "owner", "type": "address"},
      {"name": "spender", "type": "address"}
    ],
    "name": "allowance",
    "outputs": [{"type": "uint256"}],
    "type": "function"
  },
  {
    "constant": false,
    "inputs": [
      {"name": "from", "type": "address"},
      {"name": "to", "type": "address"},
      {"name": "amount", "type": "uint256"}
    ],
    "name": "transferFrom",
    "outputs": [{"type": "bool"}],
    "type": "function"
  }
];

Token Metadata Script

async function getTokenInfo(tokenAddress) {
  const web3 = new Web3('https://rpc-testnet0.beatoz.io');
  const token = new web3.beatoz.Contract(ERC20_ABI, tokenAddress);
  
  const [name, symbol, decimals, totalSupply] = await Promise.all([
    token.methods.name().call(),
    token.methods.symbol().call(),
    token.methods.decimals().call(),
    token.methods.totalSupply().call(),
  ]);
  
  const formattedSupply = parseFloat(totalSupply) / Math.pow(10, decimals);
  
  console.log('Token Info:');
  console.log('  Name:', name);
  console.log('  Symbol:', symbol);
  console.log('  Decimals:', decimals);
  console.log('  Total Supply:', formattedSupply.toLocaleString());
  
  return { name, symbol, decimals, totalSupply: formattedSupply };
}

Congratulations! You've created your own ERC-20 token on Beatoz! 🎉

Security Considerations

Important Security Tips

  • Always audit token contracts before deployment
  • Use OpenZeppelin's battle-tested implementations
  • Consider adding access controls for sensitive functions
  • Test thoroughly on devnet/testnet before mainnet

Next Steps

Beatoz Developer Portal