Solidity 智能合约编程:从 ERC20 到 DApp
Solidity 智能合约开发入门:从 ERC20 代币到去中心化应用
概述
本教程面向具备基础编程概念的开发者,带你从零掌握 Solidity 语法核心,完成 ERC20 代币合约编写、部署,并构建一个可交互的简易 DApp。你将理解智能合约的生命周期、Gas 机制、安全注意事项,以及前端如何与链上合约通信。所有示例均使用 Remix IDE 与 MetaMask,无需预先配置本地开发环境。
环境准备
必备工具
- Remix IDE:浏览器内 Solidity 开发环境,访问 remix.ethereum.org
- MetaMask:浏览器钱包插件,用于管理账户与签名交易
- 测试网络:Sepolia 或 Goerli,从水龙头获取免费测试 ETH
- 基础知识:了解区块链基本概念(区块、交易、地址)
快速体验
- 安装 MetaMask 并创建钱包,切换到 Sepolia 测试网。
- 打开 Remix,创建新工作空间,选择 “Blank” 模板。
- 在文件浏览器中新建文件
MyToken.sol,准备编写第一份合约。
Solidity 基础语法速览
合约结构与数据类型
Solidity 是静态类型语言,合约类似于面向对象中的类。主要值类型包括:
uint256(默认无符号整数)、int256address(20 字节以太坊地址)boolbytes1至bytes32string(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;
}
}
函数、修饰符与可见性
public、private、internal、external控制函数与状态变量访问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 中编译
- 选择 Solidity 编译器版本与合约匹配(如
0.8.19)。 - 点击 “Compile MyToken.sol”,确保无报错。
- 进入 “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 方案或时间锁
开发流程建议
- 使用 OpenZeppelin Contracts 作为基础库。
- 在测试网充分测试,使用 Hardhat 或 Foundry 编写自动化测试。
- 合约代码开源并提交 Etherscan 验证,增加透明度。
- 考虑委托专业的智能合约审计服务。
从代币到 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 中逐行理解,并尝试修改参数、增加功能。只有结合实际部署与前端交互,才能真正掌握智能合约开发的全貌。