Web3 实战:开发你的 ERC20 代币合约

· 7min · Paxon Qiao

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();
    }
}

部署合约

报错解决

image-20240717183613976

解决: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

image-20240717184431297

部署问题修改

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

image-20240722175318903

总结

这场 Web3 实战之旅,我们从零搭建并部署了一个符合 ERC20 标准的代币合约,验证了其在测试网上的可行性。这不仅是一次代码的实践,更是对 Web3 去中心化潜力的深度体验。无论你是想发行自己的代币,还是探索区块链开发的奥秘,本文都为你打下了坚实基础。接下来,不妨尝试优化合约功能,或将创意变为现实——你的 Web3 冒险才刚刚开始!

参考