Solidity 合约工厂模式:一次部署,无限制造「子合约」

·

为什么要用合约工厂?

合约工厂(Contract Factory)是一段 Solidity 智能合约,它利用 new 关键字在链上 动态创建其他合约实例
这种模式可以:

👉 一看就懂的合约工厂最佳实践与避坑提示

核心关键词

在接下来的示例与讲解中,我们将反复提到以下关键词:
Solidity、合约工厂、动态部署、new关键字、构造函数、地址追踪、Hardhat测试、Gas优化、状态变量初始化、智能合约设计模式


模板合约 Bank:被生产的「产品」

为了不影响阅读,示例尽可能精简,只保留关键字段。

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

contract Bank {
    uint256 public bank_funds; // 银行存款
    address public owner;      // 资产控制权
    address public deployer;   // 实际部署者(即工厂地址)

    constructor(address _owner, uint256 _funds) {
        bank_funds = _funds;
        owner = _owner;
        deployer = msg.sender; // 注意:在「工厂」内部执行时,msg.sender 变成了工厂
    }
}

工厂合约 BankFactory:流水线的「引擎」

contract BankFactory {
    Bank[] public list_of_banks; // 储存所有已创建的 Bank 地址

    function createBank(address _owner, uint256 _funds) external {
        Bank bank = new Bank(_owner, _funds); // 动态部署
        list_of_banks.push(bank);             // 记录地址
    }

    // 如果想知道已经造了多少家“银行”
    function banksCount() external view returns (uint) {
        return list_of_banks.length;
    }
}

一句话总结:一行 new Bank(...),就能在链上「印钞」一样产生新的合约地址


深入拆解工作原理

1. new 关键字到底做了什么?

2. 地址是如何生成的?

只要模板字节码和构造函数参数 一直相同,地址也会 确定性 改变吗?
答:不固定
因为 EVM 采用了 CREATE 算法:address = keccak256(rlp([deployer, nonce])),nonce 随交易递增。
如果想获得 已知地址(例如提前算好),可以使用 CREATE2,不过这超出了本文范围。

3. Gas 成本预估

在以太坊主网(2025 年 1 月数据),createBank 大约消耗 65 k–75 k gas
把模板合约单独部署一次需要 180 k gas。因此,当需求超过 3 次调用后,工厂模式的 总成本更低


Hardhat 全流程实战

第1步:初始化项目

mkdir bank-factory-demo && cd bank-factory-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

第2步:写入合约

把「模板合约 Bank + 工厂合约 BankFactory」贴进 contracts/FactoryDemo.sol

第3步:编写测试脚本 test/factory.test.js

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

describe("BankFactory 测试", () => {
  it("应成功创建三家 Bank 并验证参数", async () => {
    const [deployer, alice] = await ethers.getSigners();
    const Factory = await ethers.getContractFactory("BankFactory");
    const factory = await Factory.deploy();
    await factory.deployed();

    // 创建3个 Bank
    await factory.createBank(alice.address, 1000);
    await factory.createBank(alice.address, 2000);
    await factory.createBank(alice.address, 3000);

    // 验证地址与数据
    const addr0 = await factory.list_of_banks(0);
    const bank0 = await ethers.getContractAt("Bank", addr0);

    expect(await bank0.owner()).to.eq(alice.address);
    expect(await bank0.bank_funds()).to.eq(1000);
    expect(await bank0.deployer()).to.eq(factory.address);
  });
});

运行

npx hardhat test

若看到三行绿色 ✓,说明工厂运转正常。


场景化用例 3 连发

  1. 无代码代币生成器
    开发者写好 ERC20 Template,再写个 TokenFactory。用户只需在前端输入名称、符号、供应量,即可 一键生成专属 Token
  2. 链上任务悬赏系统
    每条任务 => 一个新合约实例存储赏金。任务完成即执行自毁,退回剩余资金。
  3. NFT 限量发行
    艺术团队提前发布 NFTCollectionFactory,粉丝每次铸币即创建一份 独立收藏合约,互不影响。

👉 查看更多真实项目的合约工厂源码与案例分析


FAQ:你真正想问的 5 个问题

Q1:new 失败会怎样?
A:合约回滚,已消耗的 Gas 不可退。若担心构造函数 reverting,可在工厂内部用 try-catch。

Q2:批量创建 100个 Bank 会不会炸 Gas?
A:会。在单 tx 内向 EVM 连续 create,会触发 区块 gas 上限。搭配 openzeppelin/Clones最小代理 模式,可把单实例开销从 65 k 降到 3 k。

Q3:如何升级 Bank 模板?
A:工厂无法直接升级已部署的 Bank,但可以通过 代理工厂 + Beacon 策略实现整体升级逻辑,后续文章详聊。

Q4:生成的 Bank 如何被销毁?
A:在 Bank 模板里加入 selfdestruct(payable(owner)),由拥有者或工厂发起即可清理状态并回收 约 24 k gas

Q5:能否提前知道新地址?
A:使用 CREATE2 工厂可实现 可预测地址,这对 State-channel 或 Layer2 批量部署非常关键。


小结与下一步

愿你每一次 new,都能造出更酷更安全的 Solidity 合约