Hardhat开发框架

Hardhat开发框架

Hardhat是一个专业的以太坊开发环境,提供了完整的开发、测试和部署工具链。本章将介绍Hardhat的核心功能和最佳实践。

环境设置

1. 安装Hardhat

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 创建新项目目录
mkdir my-dapp
cd my-dapp

# 初始化npm项目
npm init -y

# 安装Hardhat
npm install --save-dev hardhat

# 初始化Hardhat项目
npx hardhat

2. 项目结构

1
2
3
4
5
6
my-dapp/
├── contracts/           # 智能合约目录
├── scripts/            # 部署和任务脚本
├── test/               # 测试文件
├── hardhat.config.js   # Hardhat配置文件
└── package.json        # 项目配置文件

3. 基础配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-ethers");
require("@nomiclabs/hardhat-etherscan");
require("dotenv").config();

module.exports = {
    solidity: {
        version: "0.8.17",
        settings: {
            optimizer: {
                enabled: true,
                runs: 200
            }
        }
    },
    networks: {
        hardhat: {
            chainId: 1337
        },
        localhost: {
            url: "http://127.0.0.1:8545"
        },
        goerli: {
            url: `https://goerli.infura.io/v3/${process.env.INFURA_KEY}`,
            accounts: [process.env.PRIVATE_KEY]
        },
        mainnet: {
            url: `https://mainnet.infura.io/v3/${process.env.INFURA_KEY}`,
            accounts: [process.env.PRIVATE_KEY]
        }
    },
    etherscan: {
        apiKey: process.env.ETHERSCAN_API_KEY
    }
};

合约开发

1. 创建合约

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract Token is ERC20, Ownable {
    constructor(string memory name, string memory symbol) 
        ERC20(name, symbol) 
    {
        _mint(msg.sender, 1000000 * 10 ** decimals());
    }

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

2. 编译合约

1
2
3
4
5
# 编译合约
npx hardhat compile

# 清理编译文件
npx hardhat clean

3. 部署脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// scripts/deploy.js
async function main() {
    // 获取合约工厂
    const Token = await ethers.getContractFactory("Token");

    // 部署合约
    const token = await Token.deploy("MyToken", "MTK");
    await token.deployed();

    console.log("Token deployed to:", token.address);

    // 验证合约
    if (network.name !== "hardhat") {
        await hre.run("verify:verify", {
            address: token.address,
            constructorArguments: ["MyToken", "MTK"],
        });
    }
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });

测试框架

1. 单元测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// test/Token.test.js
const { expect } = require("chai");

describe("Token", function() {
    let Token;
    let token;
    let owner;
    let addr1;
    let addr2;

    beforeEach(async function() {
        // 获取合约工厂
        Token = await ethers.getContractFactory("Token");
        [owner, addr1, addr2] = await ethers.getSigners();

        // 部署合约
        token = await Token.deploy("MyToken", "MTK");
        await token.deployed();
    });

    describe("Deployment", function() {
        it("Should set the right owner", async function() {
            expect(await token.owner()).to.equal(owner.address);
        });

        it("Should assign the total supply of tokens to the owner", async function() {
            const ownerBalance = await token.balanceOf(owner.address);
            expect(await token.totalSupply()).to.equal(ownerBalance);
        });
    });

    describe("Transactions", function() {
        it("Should transfer tokens between accounts", async function() {
            // 转账
            await token.transfer(addr1.address, 50);
            expect(await token.balanceOf(addr1.address)).to.equal(50);

            // 从addr1转到addr2
            await token.connect(addr1).transfer(addr2.address, 50);
            expect(await token.balanceOf(addr2.address)).to.equal(50);
        });

        it("Should fail if sender doesn't have enough tokens", async function() {
            const initialOwnerBalance = await token.balanceOf(owner.address);

            await expect(
                token.connect(addr1).transfer(owner.address, 1)
            ).to.be.revertedWith("ERC20: transfer amount exceeds balance");

            expect(await token.balanceOf(owner.address)).to.equal(
                initialOwnerBalance
            );
        });
    });
});

2. 集成测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// test/integration/Token.test.js
describe("Token Integration", function() {
    it("Should interact with other contracts", async function() {
        // 部署代币合约
        const Token = await ethers.getContractFactory("Token");
        const token = await Token.deploy("MyToken", "MTK");

        // 部署交易所合约
        const Exchange = await ethers.getContractFactory("Exchange");
        const exchange = await Exchange.deploy();

        // 测试交互
        await token.approve(exchange.address, 1000);
        await exchange.deposit(token.address, 1000);

        expect(await token.balanceOf(exchange.address)).to.equal(1000);
    });
});

3. 测试工具

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// test/helpers.js
const { ethers } = require("hardhat");

// 时间操作
async function increaseTime(seconds) {
    await ethers.provider.send("evm_increaseTime", [seconds]);
    await ethers.provider.send("evm_mine");
}

// 区块操作
async function mineBlocks(count) {
    for (let i = 0; i < count; i++) {
        await ethers.provider.send("evm_mine");
    }
}

// 快照
async function takeSnapshot() {
    return await ethers.provider.send("evm_snapshot", []);
}

async function revertToSnapshot(id) {
    await ethers.provider.send("evm_revert", [id]);
}

module.exports = {
    increaseTime,
    mineBlocks,
    takeSnapshot,
    revertToSnapshot
};

任务和脚本

1. 自定义任务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// tasks/token.js
task("balance", "Prints account balance")
    .addParam("account", "The account's address")
    .addParam("token", "The token's address")
    .setAction(async (taskArgs, hre) => {
        const token = await hre.ethers.getContractAt("Token", taskArgs.token);
        const balance = await token.balanceOf(taskArgs.account);
        console.log(`Balance: ${hre.ethers.utils.formatEther(balance)} MTK`);
    });

task("mint", "Mints tokens to an address")
    .addParam("to", "The recipient's address")
    .addParam("amount", "The amount to mint")
    .setAction(async (taskArgs, hre) => {
        const Token = await hre.ethers.getContractFactory("Token");
        const token = Token.attach(process.env.TOKEN_ADDRESS);

        const tx = await token.mint(
            taskArgs.to,
            hre.ethers.utils.parseEther(taskArgs.amount)
        );
        await tx.wait();

        console.log(`Minted ${taskArgs.amount} tokens to ${taskArgs.to}`);
    });

2. 部署脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// scripts/deploy-all.js
async function main() {
    // 部署代币
    const Token = await ethers.getContractFactory("Token");
    const token = await Token.deploy("MyToken", "MTK");
    await token.deployed();
    console.log("Token deployed to:", token.address);

    // 部署交易所
    const Exchange = await ethers.getContractFactory("Exchange");
    const exchange = await Exchange.deploy(token.address);
    await exchange.deployed();
    console.log("Exchange deployed to:", exchange.address);

    // 部署后配置
    const MINTER_ROLE = ethers.utils.keccak256(
        ethers.utils.toUtf8Bytes("MINTER_ROLE")
    );
    await token.grantRole(MINTER_ROLE, exchange.address);

    // 保存部署信息
    const deployments = {
        token: token.address,
        exchange: exchange.address,
        network: network.name,
        timestamp: new Date().toISOString()
    };

    fs.writeFileSync(
        'deployments.json',
        JSON.stringify(deployments, null, 2)
    );
}

main();

3. 维护脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// scripts/verify-all.js
async function main() {
    const deployments = require('../deployments.json');

    // 验证代币合约
    await hre.run("verify:verify", {
        address: deployments.token,
        constructorArguments: ["MyToken", "MTK"]
    });

    // 验证交易所合约
    await hre.run("verify:verify", {
        address: deployments.exchange,
        constructorArguments: [deployments.token]
    });
}

main();

高级特性

1. Gas优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// hardhat.config.js
module.exports = {
    solidity: {
        version: "0.8.17",
        settings: {
            optimizer: {
                enabled: true,
                runs: 200,
                details: {
                    yul: true,
                    yulDetails: {
                        stackAllocation: true,
                        optimizerSteps: "dhfoDgvulfnTUtnIf"
                    }
                }
            }
        }
    }
};

2. 合约大小优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 使用库减少合约大小
library MathLib {
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "Math: addition overflow");
        return c;
    }
}

contract Token {
    using MathLib for uint256;

    function transfer(address to, uint256 amount) public {
        balances[msg.sender] = balances[msg.sender].sub(amount);
        balances[to] = balances[to].add(amount);
    }
}

3. 自动化测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// package.json
{
    "scripts": {
        "test": "hardhat test",
        "test:coverage": "hardhat coverage",
        "test:gas": "REPORT_GAS=true hardhat test"
    }
}

// hardhat.config.js
require("solidity-coverage");
require("hardhat-gas-reporter");

module.exports = {
    gasReporter: {
        enabled: process.env.REPORT_GAS ? true : false,
        currency: "USD",
        gasPrice: 100,
        coinmarketcap: process.env.COINMARKETCAP_API_KEY
    }
};

最佳实践

1. 环境配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// .env
INFURA_KEY=your_infura_key
PRIVATE_KEY=your_private_key
ETHERSCAN_API_KEY=your_etherscan_key
COINMARKETCAP_API_KEY=your_coinmarketcap_key

// .env.example
INFURA_KEY=
PRIVATE_KEY=
ETHERSCAN_API_KEY=
COINMARKETCAP_API_KEY=

// .gitignore
.env
coverage
coverage.json
typechain
deployments.json

2. 开发工作流

1
2
3
4
5
6
7
8
# 开发流程
npm install               # 安装依赖
npx hardhat compile      # 编译合约
npx hardhat test         # 运行测试
npx hardhat node         # 启动本地节点
npx hardhat run scripts/deploy.js --network localhost  # 本地部署
npx hardhat run scripts/deploy.js --network goerli     # 测试网部署
npx hardhat verify --network goerli DEPLOYED_ADDRESS   # 验证合约

3. CI/CD配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '14.x'
      - run: npm ci
      - run: npm test
      - run: npm run test:coverage

练习题

  1. 创建一个完整的代币合约项目:
1
2
3
4
# 练习:创建ERC20代币项目
npx hardhat init token-project
cd token-project
# 完成合约、测试和部署脚本
  1. 实现自动化部署脚本:
1
2
3
4
// 练习:实现部署脚本
async function deployAll() {
    // 你的代码
}
  1. 编写完整的测试套件:
1
2
3
4
// 练习:实现测试套件
describe("Token", function() {
    // 你的代码
});
  1. 创建自定义Hardhat任务:
1
2
3
4
5
// 练习:实现自定义任务
task("custom-task", "Description")
    .setAction(async () => {
        // 你的代码
    });
  1. 实现Gas优化策略:
1
2
3
4
// 练习:实现Gas优化
contract OptimizedToken {
    // 你的代码
}

参考资源

  1. Hardhat文档
  2. Hardhat教程
  3. Solidity优化技巧
  4. OpenZeppelin合约
  5. Hardhat最佳实践