为什么要用合约工厂?
合约工厂(Contract Factory)是一段 Solidity 智能合约,它利用 new 关键字在链上 动态创建其他合约实例。
这种模式可以:
- 降低用户 Gas:先部署工厂,再按需生成子合约,省下重复部署脚本的成本。
- 统一版本控制:所有子合约代码保持一致,升级只需更新工厂逻辑。
- 生成可溯源 ID:每产生一个新合约,链上都会留下唯一地址,方便审计。
核心关键词
在接下来的示例与讲解中,我们将反复提到以下关键词:
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 变成了工厂
}
}- 要点:构造函数只负责把 外部传入的参数 写入状态变量。
- 注意:
deployer记的是 工厂地址,而非最终用户的 EOA。
工厂合约 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 关键字到底做了什么?
- 在 EVM 层面 创建一个新账户(合约地址)。
- 复制模板合约的 运行时码 到新地址。
- 调用目标构造函数 并传入给定参数。
- 返回 新地址 给上层合约,存入
list_of_banks。
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 连发
- 无代码代币生成器
开发者写好 ERC20 Template,再写个 TokenFactory。用户只需在前端输入名称、符号、供应量,即可 一键生成专属 Token。 - 链上任务悬赏系统
每条任务 => 一个新合约实例存储赏金。任务完成即执行自毁,退回剩余资金。 - 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 批量部署非常关键。
小结与下一步
- 你已学会 三步搭好流水线:写模板 → 写工厂 → 调 create。
- 对比单次部署,这种模式 至少节省 50% 以上链上支出。
- 下一步可阅读 EIP-1167 最小代理 与 EIP-1967 Beacon,升级工厂艺术。
愿你每一次 new,都能造出更酷更安全的 Solidity 合约!