从零到一:构建你的第一个智能合约并部署到以太坊测试网

4 阅读1分钟

引言

在区块链技术蓬勃发展的今天,智能合约已经成为去中心化应用(DApp)的核心组件。无论你是想创建自己的代币、开发去中心化金融(DeFi)应用,还是构建NFT市场,掌握智能合约开发都是必不可少的一步。本文将从零开始,手把手教你编写、测试并部署你的第一个智能合约到以太坊测试网。

环境准备

1. 安装必要的工具

首先,我们需要安装以下开发工具:

Node.js 和 npm

# 检查是否已安装
node --version
npm --version

# 如果未安装,请访问 https://nodejs.org 下载安装

Hardhat(以太坊开发框架)

npm install --save-dev hardhat

MetaMask(浏览器钱包插件)

  • 从官方网站 metamask.io 下载并安装
  • 创建钱包并妥善保存助记词

2. 创建项目

# 创建项目目录
mkdir my-first-smart-contract
cd my-first-smart-contract

# 初始化npm项目
npm init -y

# 初始化Hardhat项目
npx hardhat init

# 选择"Create a JavaScript project"
# 一路回车使用默认配置

编写第一个智能合约

1. 理解智能合约基础

智能合约是用Solidity语言编写的,运行在以太坊虚拟机(EVM)上的程序。它类似于一个自动执行的数字协议,一旦部署就无法修改。

2. 创建简单的代币合约

contracts 目录下创建 MyToken.sol

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

contract MyToken {
    // 状态变量
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;
    
    // 地址到余额的映射
    mapping(address => uint256) public balanceOf;
    
    // 事件:用于前端监听
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Mint(address indexed to, uint256 value);
    
    // 构造函数:部署时执行一次
    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals,
        uint256 _initialSupply
    ) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
        
        // 将初始供应量分配给合约部署者
        balanceOf[msg.sender] = _initialSupply;
        totalSupply = _initialSupply;
        
        emit Transfer(address(0), msg.sender, _initialSupply);
    }
    
    // 转账函数
    function transfer(address _to, uint256 _value) public returns (bool) {
        require(_to != address(0), "Invalid address");
        require(balanceOf[msg.sender] >= _value, "Insufficient balance");
        
        balanceOf[msg.sender] -= _value;
        balanceOf[_to] += _value;
        
        emit Transfer(msg.sender, _to, _value);
        return true;
    }
    
    // 铸造新代币(仅合约所有者可用)
    function mint(address _to, uint256 _value) public {
        // 在实际项目中,这里应该添加权限控制
        balanceOf[_to] += _value;
        totalSupply += _value;
        
        emit Mint(_to, _value);
        emit Transfer(address(0), _to, _value);
    }
    
    // 查询余额
    function getBalance(address _account) public view returns (uint256) {
        return balanceOf[_account];
    }
}

3. 合约代码解析

让我们详细分析这个合约的关键部分:

状态变量

  • namesymbol:代币名称和符号
  • decimals:小数位数(通常为18)
  • totalSupply:总供应量
  • balanceOf:存储每个地址的余额

事件

  • 事件允许前端应用监听合约状态变化
  • indexed 参数使事件可过滤

安全注意事项

  • require 语句用于验证条件
  • 防止整数溢出(Solidity 0.8+ 自动检查)
  • 地址验证防止转账到零地址

编写测试用例

1. 安装测试依赖

npm install --save-dev @nomicfoundation/hardhat-toolbox

2. 创建测试文件

test 目录下创建 MyToken.test.js

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("MyToken", function () {
  let MyToken;
  let myToken;
  let owner;
  let addr1;
  let addr2;
  let addrs;

  beforeEach(async function () {
    // 获取合约工厂
    MyToken = await ethers.getContractFactory("MyToken");
    
    // 获取测试账户
    [owner, addr1, addr2, ...addrs] = await ethers.getSigners();
    
    // 部署合约
    myToken = await MyToken.deploy(
      "My Test Token",
      "MTT",
      18,
      ethers.parseEther("1000000") // 100万代币
    );
    
    await myToken.waitForDeployment();
  });

  describe("部署", function () {
    it("应该设置正确的代币信息", async function () {
      expect(await myToken.name()).to.equal("My Test Token");
      expect(await myToken.symbol()).to.equal("MTT");
      expect(await myToken.decimals()).to.equal(18);
      expect(await myToken.totalSupply()).to.equal(
        ethers.parseEther("1000000")
      );
    });

    it("应该将初始供应量分配给部署者", async function () {
      const ownerBalance = await myToken.balanceOf(owner.address);
      expect(ownerBalance).to.equal(ethers.parseEther("1000000"));
    });
  });

  describe("转账", function () {
    it("应该允许代币转账", async function () {
      // 转账100代币给addr1
      const transferAmount = ethers.parseEther("100");
      await myToken.transfer(addr1.address, transferAmount);

      // 验证余额变化
      const addr1Balance = await myToken.balanceOf(addr1.address);
      expect(addr1Balance).to.equal(transferAmount);

      const ownerBalance = await myToken.balanceOf(owner.address);
      expect(ownerBalance).to.equal(
        ethers.parseEther("1000000").sub(transferAmount)
      );
    });

    it("应该拒绝余额不足的转账", async function () {
      const initialBalance = await myToken.balanceOf(addr1.address);
      
      // addr1尝试转账,但余额为0
      await expect(
        myToken.connect(addr1).transfer(owner.address, 1)
      ).to.be.revertedWith("Insufficient balance");
    });

    it("应该拒绝转账到零地址", async function () {
      await expect(
        myToken.transfer(ethers.ZeroAddress, 1)
      ).to.be.revertedWith("Invalid address");
    });
  });

  describe("铸造", function () {
    it("应该允许铸造新代币", async function () {
      const mintAmount = ethers.parseEther("500");
      const initialSupply = await myToken.totalSupply();
      
      await myToken.mint(addr1.address, mintAmount);
      
      const newSupply = await myToken.totalSupply();
      expect(newSupply).to.equal(initialSupply.add(mintAmount));
      
      const addr1Balance = await myToken.balanceOf(addr1.address);
      expect(addr1Balance).to.equal(mintAmount);
    });
  });
});

3. 运行测试

npx hardhat test

如果一切正常,你会看到所有测试通过。

配置网络和部署脚本

1. 配置Hardhat网络

修改 hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.19",
  networks: {
    // 本地开发网络
    hardhat: {
      chainId: 31337,
    },
    // Sepolia测试网
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL || "",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
    // Goerli测试网(已弃用,仅作示例)
    goerli: {
      url: process.env.GOERLI_RPC_URL || "",
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },