Web3 实战:开发你的 ERC20 代币合约
Table of Contents
Web3 实战:开发你的 ERC20 代币合约
区块链技术正在掀起 Web3 革命,而 ERC20 代币作为以太坊生态的明星标准,让每个人都能轻松创建属于自己的数字资产。你是否好奇如何从零开始打造一个代币合约?本文将带你走进 Web3 的实战世界,通过清晰的代码示例和部署步骤,解锁 ERC20 代币开发的完整流程。无论你是新手还是开发者,这场实战之旅都将让你收获满满!
本文通过一个真实的案例,展示了如何使用 Solidity 编写并部署一个功能强大的 ERC20 代币合约 MyERC20Token。基于 OpenZeppelin 标准库,合约支持铸造、销毁、许可等功能,并在 Sepolia 测试网上成功上线。我们将深入剖析代码逻辑、部署脚本及问题解决过程,为你提供一站式的 Web3 开发实战指南。准备好动手打造你的代币了吗?
实操
合约代码
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
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/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC1363Receiver.sol";
// import "erc-payable-token/contracts/token/ERC1363/IERC1363Errors.sol";
import {ERC1363Utils} from "erc-payable-token/contracts/token/ERC1363/ERC1363Utils.sol";
contract MyERC20Token is ERC20, ERC20Burnable, Ownable, ERC20Permit, ERC20Votes {
constructor(address initialOwner)
ERC20("MyERC20Token", "MTKERC20")
Ownable(initialOwner)
ERC20Permit("MyERC20Token")
{}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
// The following functions are overrides required by Solidity.
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
super._update(from, to, value);
}
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
function transferAndcall(address to, uint256 value, bytes calldata data) public returns (bool) {
if (!transfer(to, value)) {
revert ERC1363Utils.ERC1363TransferFailed(to, value);
}
_checkOnTransferReceived(msg.sender, to, value, data);
return true;
}
function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private {
if (to.code.length == 0) {
revert ERC1363Utils.ERC1363EOAReceiver(to);
}
try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) {
if (retval != IERC1363Receiver.onTransferReceived.selector) {
revert ERC1363Utils.ERC1363InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
revert ERC1363Utils.ERC1363InvalidReceiver(to);
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
}
合约代码解析
以下是 MyERC20Token 合约的核心代码,我们将逐部分拆解它的功能和实现逻辑。
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;
- // SPDX-License-Identifier: MIT:声明代码采用 MIT 许可,允许自由使用和修改。
- // Compatible with OpenZeppelin Contracts ^5.0.0:表明代码与 OpenZeppelin 5.0.0 版本兼容,这是一个广受信赖的智能合约库。
- pragma solidity ^0.8.20:指定 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/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/interfaces/IERC1363Receiver.sol";
import {ERC1363Utils} from "erc-payable-token/contracts/token/ERC1363/ERC1363Utils.sol";
- 这些 import 语句引入了 OpenZeppelin 提供的模块化功能:
- ERC20:基础 ERC20 代币标准,提供转账、余额查询等功能。
- ERC20Burnable:扩展功能,允许销毁代币。
- Ownable:访问控制,只有合约拥有者可执行特定操作。
- ERC20Permit:支持离线签名授权(EIP-2612),提升用户体验。
- ERC20Votes:支持治理投票功能,记录代币持有者的投票权重。
- IERC721 和 IERC1363Receiver:为后续扩展(如与 NFT 或支付功能集成)预留接口。
- ERC1363Utils:支持 ERC1363 标准,提供代币转账后回调功能。
contract MyERC20Token is ERC20, ERC20Burnable, Ownable, ERC20Permit, ERC20Votes {
constructor(address initialOwner)
ERC20("MyERC20Token", "MTKERC20")
Ownable(initialOwner)
ERC20Permit("MyERC20Token")
{}
- MyERC20Token 继承了多个 OpenZeppelin 合约,集成了基础代币、销毁、权限、签名和投票功能。
- constructor:
构造函数 :- 初始化代币名称为 MyERC20Token,符号为 MTKERC20。
- 将合约拥有者设置为 initialOwner,只有此地址能调用受限函数(如铸造)。
- ERC20Permit 初始化代币名称,支持签名授权。
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
- mint 函数允许合约拥有者铸造新代币。
- 参数 to 是接收者地址,amount 是铸造数量。
- onlyOwner 修饰符限制只有拥有者可调用,调用内部 _mint 函数增加代币供应。
function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
super._update(from, to, value);
}
function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
return super.nonces(owner);
}
- 这两个函数是由于多重继承(ERC20 和 ERC20Votes、ERC20Permit)需要重写的:
- _update:更新代币转账状态,同时记录投票权重。
- nonces:返回某地址的签名计数,用于防止重放攻击。
- 使用 super 调用父合约的实现,确保功能一致性。
function transferAndcall(address to, uint256 value, bytes calldata data) public returns (bool) {
if (!transfer(to, value)) {
revert ERC1363Utils.ERC1363TransferFailed(to, value);
}
_checkOnTransferReceived(msg.sender, to, value, data);
return true;
}
- transferAndcall 实现 ERC1363 标准,支持转账后回调。
- 先调用 transfer 执行转账,若失败则抛出错误。
- 成功后调用 _checkOnTransferReceived 检查接收者是否支持回调。
function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private {
if (to.code.length == 0) {
revert ERC1363Utils.ERC1363EOAReceiver(to);
}
try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) {
if (retval != IERC1363Receiver.onTransferReceived.selector) {
revert ERC1363Utils.ERC1363InvalidReceiver(to);
}
} catch (bytes memory reason) {
if (reason.length == 0) {
revert ERC1363Utils.ERC1363InvalidReceiver(to);
} else {
assembly {
revert(add(32, reason), mload(reason))
}
}
}
}
- _checkOnTransferReceived 验证接收者是否为支持 ERC1363 的合约:
- to.code.length == 0:检查目标地址是否为普通账户(EOA),若是则报错。
- try-catch:调用接收合约的 onTransferReceived 函数,验证返回值是否正确。
- 若失败(返回值不符或异常),抛出相应错误。
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
- isContract 检查地址是否为合约,通过汇编指令 extcodesize 获取代码大小。
- 返回 true 表示是合约,false 表示普通账户,用于支持 ERC1363 的逻辑。
这段代码展示了一个功能丰富的 ERC20 代币合约:
- 标准性:基于 OpenZeppelin,确保代码安全和兼容性。
- 扩展性:支持销毁、签名授权、投票和转账回调,满足多样化需求。
- 安全性:通过 onlyOwner 和错误处理机制保护合约操作。
这段代码不仅是 Web3 开发的实战范例,也是理解 Solidity 继承和接口扩展的绝佳案例。接下来,我们将展示如何部署它到测试网,真正让你的代币“活”起来!
部署脚本
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Script, console} from "forge-std/Script.sol";
import {MyERC20Token} from "../src/MyERC20Token.sol";
contract MyERC20TokenScript is Script {
MyERC20Token public mytoken;
function setUp() public {}
function run() public {
vm.startBroadcast();
mytoken = new MyERC20Token(msg.sender);
console.log("MyERC20Token deployed to:", address(mytoken));
vm.stopBroadcast();
}
}
部署合约
报错解决
解决:forge clean
部署成功
NFTMarketHub on main [!] via ⬢ v22.1.0 via 🅒 base took 4.2s
➜ source .env
NFTMarketHub on main [!] via ⬢ v22.1.0 via 🅒 base
➜ forge script --chain sepolia script/MyERC20Token.s.sol:MyERC20TokenScript --rpc-url $SEPOLIA_RPC_URL --broadcast --account MetaMask --verify -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Enter keystore password:
Traces:
[1927821] MyERC20TokenScript::run()
├─ [0] VM::startBroadcast()
│ └─ ← [Return]
├─ [1881633] → new MyERC20Token@0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
│ └─ ← [Return] 9047 bytes of code
├─ [0] console::log("MyERC20Token deployed to:", MyERC20Token: [0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopBroadcast()
│ └─ ← [Return]
└─ ← [Stop]
Script ran successfully.
== Logs ==
MyERC20Token deployed to: 0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1
## Setting up 1 EVM.
==========================
Simulated On-chain Traces:
[1881633] → new MyERC20Token@0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1
├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
└─ ← [Return] 9047 bytes of code
==========================
Chain 11155111
Estimated gas price: 15.090597416 gwei
Estimated total gas used for script: 2722903
Estimated amount required: 0.041090232975818648 ETH
==========================
##### sepolia
✅ [Success]Hash: 0xe8faf9a7c819bd8d4a2f5ca01030d3c420df711731703988b33011b327c2f8f5
Contract Address: 0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1
Block: 6326818
Paid: 0.016047232328922354 ETH (2095191 gas * 7.659078494 gwei)
✅ Sequence #1 on sepolia | Total Paid: 0.016047232328922354 ETH (2095191 gas * avg 7.659078494 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
##
Start verification for (1) contracts
Start verifying contract `0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1` deployed on sepolia
Submitting verification for [src/MyERC20Token.sol:MyERC20Token] 0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1.
Submitting verification for [src/MyERC20Token.sol:MyERC20Token] 0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1.
Submitting verification for [src/MyERC20Token.sol:MyERC20Token] 0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1.
Submitting verification for [src/MyERC20Token.sol:MyERC20Token] 0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1.
Submitting verification for [src/MyERC20Token.sol:MyERC20Token] 0xd557Bf08136D90ed553b882Eb365e0F6b9728bB1.
Submitted contract for verification:
Response: `OK`
GUID: `cwwzeekad22aijh7sqhtlifbz11icmwignhtdf9dcbmdtfczrb`
URL: https://sepolia.etherscan.io/address/0xd557bf08136d90ed553b882eb365e0f6b9728bb1
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
All (1) contracts were verified!
Transactions saved to: /Users/qiaopengjun/Code/solidity-code/NFTMarketHub/broadcast/MyERC20Token.s.sol/11155111/run-latest.json
Sensitive values saved to: /Users/qiaopengjun/Code/solidity-code/NFTMarketHub/cache/MyERC20Token.s.sol/11155111/run-latest.json
https://sepolia.etherscan.io/address/0xd557bf08136d90ed553b882eb365e0f6b9728bb1
部署问题修改
NFTMarketHub on main [!?] via ⬢ v22.1.0 via 🅒 base took 1m 26.6s
➜ source .env
NFTMarketHub on main [!?] via ⬢ v22.1.0 via 🅒 base
➜ forge script --chain sepolia MyERC20TokenScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
[⠊] Compiling...
[⠔] Compiling 1 files with Solc 0.8.20
[⠒] Solc 0.8.20 finished in 1.44s
Compiler run successful!
Traces:
[2355225] → new MyERC20TokenScript@0x5b73C5498c1E3b4dbA84de0F1833c4a029d90519
└─ ← [Return] 11653 bytes of code
[98] MyERC20TokenScript::setUp()
└─ ← [Stop]
[2950] MyERC20TokenScript::run()
├─ [0] VM::envUint("PRIVATE_KEY") [staticcall]
│ └─ ← [Revert] failed parsing $PRIVATE_KEY as type `uint256`: missing hex prefix ("0x") for hex string
└─ ← [Revert] failed parsing $PRIVATE_KEY as type `uint256`: missing hex prefix ("0x") for hex string
Error:
script failed: failed parsing $PRIVATE_KEY as type `uint256`: missing hex prefix ("0x") for hex string
NFTMarketHub on main [!?] via ⬢ v22.1.0 via 🅒 base took 11.7s
➜ forge script --chain sepolia MyERC20TokenScript --rpc-url $SEPOLIA_RPC_URL --broadcast --verify -vvvv
[⠊] Compiling...
No files changed, compilation skipped
Traces:
[1929276] MyERC20TokenScript::run()
├─ [0] VM::envUint("PRIVATE_KEY") [staticcall]
│ └─ ← [Return] <env var value>
├─ [0] VM::envAddress("ACCOUNT_ADDRESS") [staticcall]
│ └─ ← [Return] <env var value>
├─ [0] VM::startBroadcast(<pk>)
│ └─ ← [Return]
├─ [1881633] → new MyERC20Token@0xc32cE2198B123D1c1F7FD3A9f54Bff9f975819Fa
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: 0x750Ea21c1e98CcED0d4557196B6f4a5974CCB6f5)
│ └─ ← [Return] 9047 bytes of code
├─ [0] console::log("deployerAccountAddress :", 0x750Ea21c1e98CcED0d4557196B6f4a5974CCB6f5) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("MyERC20Token deployed to:", MyERC20Token: [0xc32cE2198B123D1c1F7FD3A9f54Bff9f975819Fa]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::stopBroadcast()
│ └─ ← [Return]
└─ ← [Stop]
Script ran successfully.
== Logs ==
deployerAccountAddress : 0x750Ea21c1e98CcED0d4557196B6f4a5974CCB6f5
MyERC20Token deployed to: 0xc32cE2198B123D1c1F7FD3A9f54Bff9f975819Fa
## Setting up 1 EVM.
==========================
Simulated On-chain Traces:
[1881633] → new MyERC20Token@0xc32cE2198B123D1c1F7FD3A9f54Bff9f975819Fa
├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: 0x750Ea21c1e98CcED0d4557196B6f4a5974CCB6f5)
└─ ← [Return] 9047 bytes of code
==========================
Chain 11155111
Estimated gas price: 31.484721969 gwei
Estimated total gas used for script: 2722903
Estimated amount required: 0.085729843903556007 ETH
==========================
##### sepolia
✅ [Success]Hash: 0x6981a969928123236332cf8a1ccab58c202ccb1e056d4f99daca8d2b881749f0
Contract Address: 0xc32cE2198B123D1c1F7FD3A9f54Bff9f975819Fa
Block: 6355495
Paid: 0.032185872758241342 ETH (2095191 gas * 15.361784562 gwei)
✅ Sequence #1 on sepolia | Total Paid: 0.032185872758241342 ETH (2095191 gas * avg 15.361784562 gwei)
==========================
ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
##
Start verification for (1) contracts
Start verifying contract `0xc32cE2198B123D1c1F7FD3A9f54Bff9f975819Fa` deployed on sepolia
Submitting verification for [src/MyERC20Token.sol:MyERC20Token] 0xc32cE2198B123D1c1F7FD3A9f54Bff9f975819Fa.
Submitted contract for verification:
Response: `OK`
GUID: `nrfj275cavnnxjapk9jy8xl7qudvynqn7vkashvfkv2kkfzws7`
URL: https://sepolia.etherscan.io/address/0xc32ce2198b123d1c1f7fd3a9f54bff9f975819fa
Contract verification status:
Response: `NOTOK`
Details: `Pending in queue`
Contract verification status:
Response: `OK`
Details: `Pass - Verified`
Contract successfully verified
All (1) contracts were verified!
Transactions saved to: /Users/qiaopengjun/Code/solidity-code/NFTMarketHub/broadcast/MyERC20Token.s.sol/11155111/run-latest.json
Sensitive values saved to: /Users/qiaopengjun/Code/solidity-code/NFTMarketHub/cache/MyERC20Token.s.sol/11155111/run-latest.json
NFTMarketHub on main [!?] via ⬢ v22.1.0 via 🅒 base took 48.0s
➜
https://sepolia.etherscan.io/address/0xc32ce2198b123d1c1f7fd3a9f54bff9f975819fa#code
总结
这场 Web3 实战之旅,我们从零搭建并部署了一个符合 ERC20 标准的代币合约,验证了其在测试网上的可行性。这不仅是一次代码的实践,更是对 Web3 去中心化潜力的深度体验。无论你是想发行自己的代币,还是探索区块链开发的奥秘,本文都为你打下了坚实基础。接下来,不妨尝试优化合约功能,或将创意变为现实——你的 Web3 冒险才刚刚开始!
参考
- https://sepolia.etherscan.io/address/0xc32ce2198b123d1c1f7fd3a9f54bff9f975819fa#code
- https://learnblockchain.cn/docs/solidity/
- https://solidity-by-example.org/
- https://remix.ethereum.org/#lang=en&optimize=false&runs=200&evmVersion=null&version=soljson-v0.8.18+commit.87f61d96.js
- https://etherscan.io/
- https://book.getfoundry.sh/