Solidity 智能合约编程:从 ERC20 到 DApp

FreeGuideOnline 最新 2026-06-12

Solidity 智能合约开发入门:从 ERC20 代币到去中心化应用

概述

本教程面向具备基础编程概念的开发者,带你从零掌握 Solidity 语法核心,完成 ERC20 代币合约编写、部署,并构建一个可交互的简易 DApp。你将理解智能合约的生命周期、Gas 机制、安全注意事项,以及前端如何与链上合约通信。所有示例均使用 Remix IDE 与 MetaMask,无需预先配置本地开发环境。

环境准备

必备工具

  • Remix IDE:浏览器内 Solidity 开发环境,访问 remix.ethereum.org
  • MetaMask:浏览器钱包插件,用于管理账户与签名交易
  • 测试网络:Sepolia 或 Goerli,从水龙头获取免费测试 ETH
  • 基础知识:了解区块链基本概念(区块、交易、地址)

快速体验

  1. 安装 MetaMask 并创建钱包,切换到 Sepolia 测试网。
  2. 打开 Remix,创建新工作空间,选择 “Blank” 模板。
  3. 在文件浏览器中新建文件 MyToken.sol,准备编写第一份合约。

Solidity 基础语法速览

合约结构与数据类型

Solidity 是静态类型语言,合约类似于面向对象中的类。主要值类型包括:

  • uint256(默认无符号整数)、int256
  • address(20 字节以太坊地址)
  • bool
  • bytes1bytes32
  • string(UTF-8 编码的动态数组)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract BasicTypes {
    uint public total = 100;
    address public owner;
    bool public isActive = true;
    bytes32 public hash = keccak256(abi.encodePacked("hello"));
    string public greeting = "Hola";

    constructor() {
        owner = msg.sender;
    }
}

函数、修饰符与可见性

  • publicprivateinternalexternal 控制函数与状态变量访问
  • view:不修改状态,可读取状态变量
  • pure:既不读取也不修改状态
  • payable:允许函数接收以太币
  • 修饰符(modifier)可用于权限控制
modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

function changeGreeting(string memory _newGreeting) public onlyOwner {
    greeting = _newGreeting;
}

function getOwnerBalance() public view returns(uint) {
    return owner.balance;
}

映射与数组

  • mapping(address => uint) public balances; 存储地址到余额的关联
  • 动态数组 uint[] public numbers;,使用 push 添加元素
  • 内置结构体 struct 可自定义复杂数据
struct Payment {
    address payer;
    uint amount;
    uint timestamp;
}
Payment[] public payments;

function addPayment(uint _amount) public {
    payments.push(Payment(msg.sender, _amount, block.timestamp));
}

深入理解 ERC20 代币标准

ERC20 接口定义

ERC20 是以太坊上同质化代币的标准接口,必须实现以下函数:

  • totalSupply():代币总供应量
  • balanceOf(address account):查询地址余额
  • transfer(address to, uint amount):转移代币
  • approve(address spender, uint amount):授权他人使用代币
  • allowance(address owner, address spender):查询授权额度
  • transferFrom(address from, address to, uint amount):被授权地址执行转移

此外还有两个事件:

  • event Transfer(address indexed from, address indexed to, uint value);
  • event Approval(address indexed owner, address indexed spender, uint value);

从零编写 ERC20 合约

我们基于 OpenZeppelin 的 ERC20 实现来讲解,但先手写一个最小化版本,理解原理。

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

contract SimpleERC20 {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint8 public decimals = 18;
    uint256 public totalSupply;
    
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
    
    constructor(uint256 _initialSupply) {
        totalSupply = _initialSupply * 10 ** decimals;
        balanceOf[msg.sender] = totalSupply;
        emit Transfer(address(0), msg.sender, totalSupply);
    }
    
    function transfer(address _to, uint256 _value) public returns (bool success) {
        require(balanceOf[msg.sender] >= _value, "Insufficient balance");
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
        emit Transfer(msg.sender, _to, _value);
        return true;
    }
    
    function approve(address _spender, uint256 _value) public returns (bool success) {
        allowance[msg.sender][_spender] = _value;
        emit Approval(msg.sender, _spender, _value);
        return true;
    }
    
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        require(balanceOf[_from] >= _value, "Insufficient balance");
        require(allowance[_from][msg.sender] >= _value, "Allowance exceeded");
        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        allowance[_from][msg.sender] -= _value;
        emit Transfer(_from, _to, _value);
        return true;
    }
}

使用 OpenZeppelin 安全实现

实际项目强烈推荐使用经过审计的库。在 Remix 中导入 OpenZeppelin:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.0/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }
}

该实现继承了所有标准函数,并通过 _mint 安全铸造代币,避免整型溢出风险。

合约编译、部署与交互

在 Remix 中编译

  1. 选择 Solidity 编译器版本与合约匹配(如 0.8.19)。
  2. 点击 “Compile MyToken.sol”,确保无报错。
  3. 进入 “Deploy & Run Transactions” 插件。

部署到测试网

  • 环境选择 “Injected Provider – MetaMask”,Remix 连接钱包。
  • 确认 MetaMask 当前为 Sepolia 测试网。
  • 在 “Contract” 下拉框选择 “MyToken”。
  • “Deploy” 输入构造参数(如 1000000 表示 100 万代币)。
  • 点击 “Transact”,MetaMask 弹出确认交易,支付 Gas 费后等待上链。

调用合约函数

部署成功后,Remix 面板会出现已部署合约实例,展开即可看到所有公开函数。

  • 点击 balanceOf,填入你的地址,查询代币余额。
  • 调用 transfer,填入其他地址与数量,执行转账。
  • 查看交易日志中的 Transfer 事件。

构建简易 DApp 前端

技术栈

  • HTML/JavaScript:轻量原生方案
  • ethers.js v6:与以太坊交互的库,通过 CDN 引入
  • MetaMask 作为注入的 provider

连接钱包与读取数据

创建一个 index.html 文件,核心逻辑:

<!DOCTYPE html>
<html>
<head>
    <title>MyToken DApp</title>
</head>
<body>
    <h2>MyToken 余额查询</h2>
    <button id="connectBtn">连接钱包</button>
    <p>账户: <span id="account"></span></p>
    <p>余额: <span id="balance"></span> MTK</p>

    <script src="https://cdn.jsdelivr.net/npm/ethers@6.7.1/dist/ethers.min.js"></script>
    <script>
        const contractAddress = "0x你的合约地址";
        const contractABI = [ ... ]; // 从 Remix 编译详情复制 ABI
        
        let provider, signer, contract;
        document.getElementById('connectBtn').addEventListener('click', async () => {
            if (window.ethereum) {
                await window.ethereum.request({ method: 'eth_requestAccounts' });
                provider = new ethers.BrowserProvider(window.ethereum);
                signer = await provider.getSigner();
                const address = await signer.getAddress();
                document.getElementById('account').textContent = address;
                
                contract = new ethers.Contract(contractAddress, contractABI, signer);
                const balance = await contract.balanceOf(address);
                const decimals = 18;
                document.getElementById('balance').textContent = ethers.formatUnits(balance, decimals);
            } else {
                alert('请安装 MetaMask');
            }
        });
    </script>
</body>
</html>

发送代币转账

增加转账功能:

<input type="text" id="toAddress" placeholder="接收地址">
<input type="number" id="amount" placeholder="数量">
<button id="transferBtn">转账</button>
document.getElementById('transferBtn').addEventListener('click', async () => {
    const to = document.getElementById('toAddress').value;
    const amount = document.getElementById('amount').value;
    const decimals = 18;
    const tx = await contract.transfer(to, ethers.parseUnits(amount, decimals));
    await tx.wait();
    alert('转账成功');
    // 刷新余额...
});

本地运行 DApp

  • 使用 VS Code 的 Live Server 或 npx http-server 启动静态服务。
  • 注意:ABI 需从 Remix 的 “Compilation Details” 中完整复制,保持与合约一致。

进阶话题与安全实践

Gas 优化

  • 使用 calldata 替代 memory 用于外部函数参数(只读)
  • 合并多个相同类型的变量以节省存储槽
  • 避免在循环中修改状态变量
  • 开启编译优化器(Optimizer runs: 200)

常见攻击防范

  • 重入攻击:使用 Checks-Effects-Interactions 模式,或引入 ReentrancyGuard
  • 整数溢出:Solidity 0.8+ 默认检查,无需 SafeMath
  • 未授权访问:严格使用 onlyOwner 或基于角色的访问控制
  • 前端运行:敏感操作考虑使用 commit-reveal 方案或时间锁

开发流程建议

  1. 使用 OpenZeppelin Contracts 作为基础库。
  2. 在测试网充分测试,使用 Hardhat 或 Foundry 编写自动化测试。
  3. 合约代码开源并提交 Etherscan 验证,增加透明度。
  4. 考虑委托专业的智能合约审计服务。

从代币到 DApp 的拓展思路

掌握了 ERC20 与基础 DApp 交互后,可以进一步探索:

  • 实现代币水龙头合约,用户每日领取测试代币
  • 结合 ERC721(NFT)做质押挖矿
  • 使用 Uniswap V2 接口添加流动性
  • 集成预言机(Chainlink)获取价格喂价
  • 部署到 L2 网络(Arbitrum, Optimism)降低交易成本

常见问题

Q:为什么 Remix 报错 “gas estimation failed”?
A:可能是 require 条件不满足,或合约构造函数参数错误。检查调用参数与网络选择。

Q:MetaMask 交易一直 Pending?
A:Gas 费设置过低,可在 MetaMask 中加速交易或取消,适当提高 Gas Price。

Q:前端无法读取合约数据?
A:检查合约地址和 ABI 是否正确,确认网络一致。浏览器控制台查看详细错误。

总结

本教程覆盖了 Solidity 核心语法、ERC20 标准实现、合约部署流程以及使用 ethers.js 构建前端 DApp。动手实践是学习的最佳途径,建议将代码复制到 Remix 中逐行理解,并尝试修改参数、增加功能。只有结合实际部署与前端交互,才能真正掌握智能合约开发的全貌。