先搭好你的Solidity开发环境
要写Solidity代码,第一步得把环境配对——不然写好的代码要么跑不起来,要么报错一堆。现在行业里最流行的开发框架是Hardhat(比Truffle更轻量、调试更方便),咱们直接用它:

- 装基础工具:先确保电脑有Node.js(推荐18.x版本),打开终端输
node -v
能看到版本号就行。 - 初始化Hardhat项目:新建个文件夹(比如
my-solidity-project
),cd进去后输:npm init -y npm install --save-dev hardhat npx hardhat init
选“Create a JavaScript project”(JS比TS更适合新手),然后一路回车确认安装依赖。
- 装辅助工具:VS Code里装两个插件——
Solidity Extended
(语法高亮+提示)、Hardhat for VS Code
(一键运行测试/部署),写代码时能省超多时间。 - 验证环境:终端输
npx hardhat test
,如果看到“0 passing”说明环境没问题(还没写测试用例呢)。
嫌麻烦?直接用在线IDE Remix(remix.ethereum.org)——不用装任何东西,打开就能写代码,但适合快速测试,正经开发还是得用Hardhat。
Solidity核心语法:搞懂这些再写代码
Solidity是静态类型语言,语法像JavaScript,但多了区块链特有的概念(比如address
、mapping
)。先记几个核心点:
1. 变量类型(别搞混了!)
类型 | 说明 | 例子 |
---|---|---|
uint |
无符号整数(默认uint256 ) |
uint256 totalSupply; |
address |
区块链地址(20字节) | address owner; |
mapping |
键值对存储(类似JS的Object,但不可遍历) | mapping(address => uint) balances; |
string |
字符串(注意:Solidity里字符串处理效率低,尽量少用) | string public name = "MyToken"; |
重点:mapping
是智能合约存储数据的核心——比如存用户余额,用mapping(address => uint) balances
就对了,每个地址对应一个余额。
2. 函数修饰符(控制函数权限和行为)
public
:任何人/合约都能调用;private
:只有合约内部能调用;view
:不修改合约状态(比如查余额),不用花gas;pure
:不读也不修改合约状态(比如计算两个数之和);payable
:允许调用时打ETH(比如众筹合约)。
例子:查余额的函数必须加view
:
function balanceOf(address account) public view returns (uint) {
return balances[account];
}
3. 错误处理(别让合约跑崩!)
Solidity里用这三个关键词处理错误:
– require
:检查条件(比如“余额足够吗?”),不满足就回滚交易,还能写提示信息;
– revert
:主动回滚交易(比require
更灵活,适合复杂条件);
– assert
:检查合约内部逻辑(比如“总 supply 不能变”),触发说明合约有bug。
正确用法:转移代币时先检查地址和余额:
require(sender != address(0), "不能从0地址转");
require(to != address(0), "不能转到0地址");
require(balances[sender] >= amount, "余额不够");
实战:写一个能跑的ERC20代币合约
ERC20是最常用的代币标准(比如USDT、ETH都是ERC20),咱们写个简化版的——能发币、转币、查余额。
完整代码(直接复制能用)
// SPDX-License-Identifier: MIT(必须加,说明许可证)
pragma solidity ^0.8.0; // 编译器版本(0.8+自动防整数溢出)
contract MyToken {
// 代币基本信息(ERC20标准要求)
string public constant name = "MyToken"; // 代币名
string public constant symbol = "MTK"; // 代币符号
uint8 public constant decimals = 18; // 小数位数(和ETH一样)
uint256 private totalSupply_; // 总供应量
// 存用户余额(ERC20核心)
mapping(address => uint256) private balances;
// 存授权额度(比如A允许B花他的代币)
mapping(address => mapping(address => uint256)) private allowances;
// 事件(必须emit,让前端能监听交易)
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// 构造函数(部署时执行,初始化总 supply)
constructor(uint256 initialSupply) {
totalSupply_ = initialSupply * 10 ** uint256(decimals); // 把初始供应转成最小单位(比如1000 MTK = 1000*1e18)
balances[msg.sender] = totalSupply_; // 把总 supply 给部署者
emit Transfer(address(0), msg.sender, totalSupply_); // 发事件
}
// 查总 supply(ERC20标准函数)
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
// 查用户余额(ERC20标准函数)
function balanceOf(address account) public view returns (uint256) {
return balances[account];
}
// 转代币(ERC20标准函数)
function transfer(address to, uint256 amount) public returns (bool) {
address sender = msg.sender;
// 检查三个条件: sender不是0地址、to不是0地址、余额够
require(sender != address(0), "Transfer from zero address");
require(to != address(0), "Transfer to zero address");
require(balances[sender] >= amount, "Insufficient balance");
// 更新余额
balances[sender] -= amount;
balances[to] += amount;
emit Transfer(sender, to, amount); // 一定要发事件!不然前端看不到交易
return true;
}
// 授权别人花自己的代币(ERC20标准函数)
function approve(address spender, uint256 amount) public returns (bool) {
address owner = msg.sender;
require(owner != address(0), "Approve from zero address");
require(spender != address(0), "Approve to zero address");
allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
return true;
}
// 用授权的额度转代币(比如B花A的代币)
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
address spender = msg.sender;
// 检查授权额度够不够
require(allowances[from][spender] >= amount, "Allowance not enough");
// 复用transfer的逻辑(省代码)
allowances[from][spender] -= amount;
_transfer(from, to, amount); // 自己写个内部函数封装转移逻辑
return true;
}
// 内部函数(private,只用在合约内部)
function _transfer(address from, address to, uint256 amount) private {
require(from != address(0), "Transfer from zero address");
require(to != address(0), "Transfer to zero address");
require(balances[from] >= amount, "Insufficient balance");
balances[from] -= amount;
balances[to] += amount;
emit Transfer(from, to, amount);
}
}
注意:
– 每写一个函数都要想“要不要加view
/pure
?”——加对了能省gas;
– 事件一定要emit
——不然前端(比如DApp)没法监听交易状态;
– 构造函数里的msg.sender
是部署合约的地址(也就是代币的发行者)。
测试:别等部署了才发现bug
写好合约先测试!Hardhat自带Mocha测试框架,直接写测试用例:
1. 新建测试文件
在test
文件夹里建MyToken.test.js
,内容:
const { expect } = require("chai"); // 断言库,检查结果对不对
const { ethers } = require("hardhat"); // Hardhat的ETH工具库
describe("MyToken", function () {
let MyToken, myToken, owner, addr1, addr2; // 定义全局变量
// 每个测试用例前都执行(初始化合约)
beforeEach(async function () {
MyToken = await ethers.getContractFactory("MyToken"); // 获取合约工厂
[owner, addr1, addr2] = await ethers.getSigners(); // 获取测试账号(Hardhat自动生成)
myToken = await MyToken.deploy(10000); // 部署合约,初始供应10000 MTK(注意:实际是10000*1e18)
await myToken.deployed(); // 等合约部署完成
});
// 测试1:检查初始余额
it("部署者应该有全部代币", async function () {
const ownerBalance = await myToken.balanceOf(owner.address);
expect(ownerBalance).to.equal(ethers.parseUnits("10000", 18)); // 转成1e18单位
});
// 测试2:转移代币是否有效
it("能在账号间转代币", async function () {
// owner转1000 MTK给addr1
await myToken.transfer(addr1.address, ethers.parseUnits("1000", 18));
expect(await myToken.balanceOf(addr1.address)).to.equal(ethers.parseUnits("1000", 18));
// addr1转500 MTK给addr2
await myToken.connect(addr1).transfer(addr2.address, ethers.parseUnits("500", 18));
expect(await myToken.balanceOf(addr2.address)).to.equal(ethers.parseUnits("500", 18));
});
// 测试3:授权额度是否有效
it("授权后能花别人的代币", async function () {
// owner授权addr1花2000 MTK
await myToken.approve(addr1.address, ethers.parseUnits("2000", 18));
// addr1用授权额度转1500 MTK给addr2
await myToken.connect(addr1).transferFrom(owner.address, addr2.address, ethers.parseUnits("1500", 18));
// 检查addr2的余额
expect(await myToken.balanceOf(addr2.address)).to.equal(ethers.parseUnits("1500", 18));
// 检查剩余授权额度(2000-1500=500)
expect(await myToken.allowance(owner.address, addr1.address)).to.equal(ethers.parseUnits("500", 18));
});
});
2. 运行测试
终端输npx hardhat test
,看到3个“passing”说明测试通过!如果失败,看报错信息——比如“Insufficient balance”,说明转移的金额超过了余额。
部署:把合约推上区块链
测试通过后,就能部署到真实的区块链了(先⽤测试网练手,比如Goerli或Sepolia,不用花真钱)。
1. 配置Hardhat
打开hardhat.config.js
,加测试网配置:
require("@nomicfoundation/hardhat-toolbox");
// 从.env文件读私钥和Alchemy API(别把私钥直接写在代码里!)
require("dotenv").config();
module.exports = {
solidity: "0.8.19", // 编译器版本要和合约里的一致
networks: {
goerli: {
url: `https://eth-goerli.alchemyapi.io/v2/${process.env.ALCHEMY_API_KEY}`, // Alchemy的Goerli节点(比Infura稳定)
accounts: [process.env.PRIVATE_KEY] // 你的测试网账号私钥(要先充点Goerli ETH,用faucet.alchemy.com领)
}
}
};
注意:
– 私钥绝对不能传github!用dotenv
存到.env
文件里(.gitignore
要加.env
);
– Alchemy API Key要去alchemy.com注册,免费的就行。
2. 写部署脚本
在scripts
文件夹建deploy.js
:
async function main() {
const MyToken = await ethers.getContractFactory("MyToken");
console.log("Deploying MyToken...");
const myToken = await MyToken.deploy(10000); // 初始供应10000 MTK
await myToken.deployed();
console.log("MyToken deployed to:", myToken.address); // 输出合约地址
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
3. 部署到Goerli测试网
终端输:
npx hardhat run scripts/deploy.js --network goerli
等几分钟,看到“ MyToken deployed to: 0x… ”就成功了!去Goerli Etherscan(goerli.etherscan.io)查这个地址,能看到合约的所有信息。
那些新手常踩的Solidity坑
- 忘记处理0地址:比如
transfer
函数没检查to != address(0)
,会导致代币永久丢失; - 用错
msg.sender
:在transferFrom
函数里,msg.sender
是调用者(比如B),不是代币所有者(比如A); - 整数溢出:Solidity 0.8+自动处理了,但如果用0.7及以下版本,要加
SafeMath
库; - 不测试就部署:比如没测
transferFrom
函数,部署后发现授权额度没用; - gas费用估算错:比如函数里用了
for
循环(Solidity里循环效率极低,gas费会爆炸),尽量避免循环。
最后:去哪里学更多?
- 官方文档:Solidity docs(docs.soliditylang.org)——最权威,别信那些过时的博客;
- 练习项目:写个NFT合约(ERC721)、写个众筹合约(比如ICO);
- 社区:GitHub的Solidity仓库(github.com/ethereum/solidity)、Discord的Hardhat社区(discord.gg/hardhat)。
其实Solidity不难,难的是“写安全的合约”——每一行代码都要想“有没有漏洞?”。慢慢来,先把ERC20写熟,再碰更复杂的合约!
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/330