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
TestnetCreate 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
