發行 ERC-20 代幣
ERC-20 是以太坊上最廣泛使用的代幣標準。所有在以太坊上的加密货币——USDT、UNI、LINK——都是 ERC-20 代幣。
ERC-20 標準介面
一個標準的 ERC-20 代幣必須實現以下功能:
| 函式 | 說明 |
|------|------|
| totalSupply() | 回傳代幣總供應量 |
| balanceOf(address) | 查詢指定地址的餘額 |
| transfer(to, amount) | 轉帳 |
| approve(spender, amount) | 授權他人動用你的代幣 |
| transferFrom(from, to, amount) | 在授權範圍內代扣轉帳 |
| allowance(owner, spender) | 查詢授權額度 |
使用 OpenZeppelin 發行代幣
OpenZeppelin 提供了經過嚴格審計的標準合約實作。我們不需要從零寫起,直接繼承即可:
// contracts/VibeToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
contract VibeToken is ERC20, ERC20Burnable, Ownable, ERC20Pausable {
// 交易稅率(3% = 300,基數為 10000)
uint256 public constant TAX_RATE = 300;
uint256 public constant TAX_DENOMINATOR = 10000;
// 金庫地址(稅金收集地址)
address public treasuryAddress;
// 免稅地址列表
mapping(address => bool) public isExcludedFromTax;
// 事件
event TaxCharged(address indexed from, address indexed to, uint256 amount, uint256 tax);
event TreasuryUpdated(address indexed newTreasury);
event TaxExclusionUpdated(address indexed account, bool excluded);
constructor(
address _treasuryAddress
) ERC20("Vibe Token", "VIBE") Ownable(msg.sender) {
require(_treasuryAddress != address(0), "金庫地址不能為空");
treasuryAddress = _treasuryAddress;
// 鑄造 1,000,000 枚代幣給合約擁有者
uint256 initialSupply = 1_000_000 * 10 ** decimals();
_mint(msg.sender, initialSupply);
// 合約擁有者免稅
isExcludedFromTax[msg.sender] = true;
}
// 覆寫 decimals,設為 18 位(ERC-20 標準)
function decimals() public pure virtual override returns (uint8) {
return 18;
}
// 覆寫 transfer 以加入交易稅
function transfer(
address to,
uint256 amount
) public virtual override returns (bool) {
address owner = _msgSender();
if (isExcludedFromTax[owner] || isExcludedFromTax[to]) {
// 免稅轉帳
return super.transfer(to, amount);
}
// 計算稅金
uint256 tax = (amount * TAX_RATE) / TAX_DENOMINATOR;
uint256 amountAfterTax = amount - tax;
// 扣稅
super.transfer(treasuryAddress, tax);
emit TaxCharged(owner, to, amountAfterTax, tax);
// 轉帳剩餘
return super.transfer(to, amountAfterTax);
}
// 覆寫 transferFrom
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
if (isExcludedFromTax[from] || isExcludedFromTax[to]) {
return super.transferFrom(from, to, amount);
}
uint256 tax = (amount * TAX_RATE) / TAX_DENOMINATOR;
uint256 amountAfterTax = amount - tax;
super.transferFrom(from, treasuryAddress, tax);
emit TaxCharged(from, to, amountAfterTax, tax);
return super.transferFrom(from, to, amountAfterTax);
}
// 設定金庫地址
function updateTreasury(address _newTreasury) external onlyOwner {
require(_newTreasury != address(0), "金庫地址不能為空");
treasuryAddress = _newTreasury;
emit TreasuryUpdated(_newTreasury);
}
// 設定免稅地址
function setTaxExclusion(address account, bool excluded) external onlyOwner {
isExcludedFromTax[account] = excluded;
emit TaxExclusionUpdated(account, excluded);
}
// 鑄造新代幣
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
// 暫停所有轉帳(緊急情況)
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// OpenZeppelin 需要這個來處理多繼承
function _update(
address from,
address to,
uint256 value
) internal override(ERC20, ERC20Pausable) {
super._update(from, to, value);
}
}
編譯與部署
// scripts/deploy_token.js
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log("部署者地址:", deployer.address);
console.log("部署者餘額:", hre.ethers.formatEther(
await hre.ethers.provider.getBalance(deployer.address)
), "ETH");
// 部署 VibeToken
const VibeToken = await hre.ethers.getContractFactory("VibeToken");
const token = await VibeToken.deploy(deployer.address);
await token.waitForDeployment();
const tokenAddress = await token.getAddress();
console.log("VibeToken 部署地址:", tokenAddress);
// 查詢總供應量
const totalSupply = await token.totalSupply();
console.log("總供應量:", hre.ethers.formatEther(totalSupply), "VIBE");
// 查詢部署者餘額
const balance = await token.balanceOf(deployer.address);
console.log("部署者餘額:", hre.ethers.formatEther(balance), "VIBE");
// 寫入部署資訊供前端使用
const fs = require("fs");
const deploymentInfo = {
tokenAddress: tokenAddress,
deployer: deployer.address,
totalSupply: hre.ethers.formatEther(totalSupply),
network: hre.network.name,
};
fs.writeFileSync(
"deployment_info.json",
JSON.stringify(deploymentInfo, null, 2)
);
console.log("部署資訊已儲存到 deployment_info.json");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
# 部署到本地測試網
npx hardhat run scripts/deploy_token.js
# 部署到 Sepolia 測試網
npx hardhat run scripts/deploy_token.js --network sepolia
在 Etherscan 驗證合約
# 安裝 hardhat-etherscan
npm install --save-dev @nomicfoundation/hardhat-verify
# 在 hardhat.config.js 中加入 Etherscan API Key
// hardhat.config.js
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
npx hardhat verify --network sepolia <合約地址> <金庫地址>
使用 Vibe Coding 發行代幣
🔥 【代幣發行詠唱範例】
「請幫我發行一個 ERC-20 代幣,包含以下功能:1. 代幣名稱 MyToken,代號 MTK,總量 10 億枚。2. 每筆交易收取 2% 手續費,自動銷毀 (burn)。3. 擁有者可以鑄造新代幣。4. 擁有者可以暫停交易。5. 部署到 Sepolia 測試網。6. 在 Etherscan 上驗證合約。」
本日總結
在本章中,你學到了:
- ✅ ERC-20 標準:代幣的統一介面規範
- ✅ OpenZeppelin 庫:使用經過審計的標準合約
- ✅ 交易稅機制:每筆交易自動扣稅轉到金庫
- ✅ 鑄造與銷毀:控制代幣的供應量
- ✅ 緊急暫停:Pausable 機制
- ✅ 部署與驗證:部署到測試網並在 Etherscan 驗證
下一章,我們將發行自己的 NFT!