Solidity智能合约开发实战指南:从环境到部署的全流程

先搭好你的Solidity开发环境

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

Solidity智能合约开发实战指南:从环境到部署的全流程

  1. 装基础工具:先确保电脑有Node.js(推荐18.x版本),打开终端输node -v能看到版本号就行。
  2. 初始化Hardhat项目:新建个文件夹(比如my-solidity-project),cd进去后输:
    npm init -y
    npm install --save-dev hardhat
    npx hardhat init
    

    选“Create a JavaScript project”(JS比TS更适合新手),然后一路回车确认安装依赖。

  3. 装辅助工具:VS Code里装两个插件——Solidity Extended(语法高亮+提示)、Hardhat for VS Code(一键运行测试/部署),写代码时能省超多时间。
  4. 验证环境:终端输npx hardhat test,如果看到“0 passing”说明环境没问题(还没写测试用例呢)。

嫌麻烦?直接用在线IDE Remix(remix.ethereum.org)——不用装任何东西,打开就能写代码,但适合快速测试,正经开发还是得用Hardhat。

Solidity核心语法:搞懂这些再写代码

Solidity是静态类型语言,语法像JavaScript,但多了区块链特有的概念(比如addressmapping)。先记几个核心点:

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坑

  1. 忘记处理0地址:比如transfer函数没检查to != address(0),会导致代币永久丢失;
  2. 用错msg.sender:在transferFrom函数里,msg.sender是调用者(比如B),不是代币所有者(比如A);
  3. 整数溢出:Solidity 0.8+自动处理了,但如果用0.7及以下版本,要加SafeMath库;
  4. 不测试就部署:比如没测transferFrom函数,部署后发现授权额度没用;
  5. 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

(0)