工厂合约

70 阅读6分钟

介绍

随着铭文(Inscription)和链上资产发行的火热,越来越多人希望能一键批量发行属于自己的Token。为此,实践了一个基于Solidity的"铭文工厂"的简单合约,支持用户自助铸造独立的ERC20 Token,并具备离线授权(permit)等现代特性。

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

import "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract InscriptionToken is Initializable, ERC20PermitUpgradeable {
    function initialize(
        string memory name_,
        string memory symbol_,
        address initialOwner_,
        uint256 initialSupply_
    ) public initializer {
        __ERC20_init(name_, symbol_);
        __ERC20Permit_init(name_);
        _mint(initialOwner_, initialSupply_);
    }
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

import "@openzeppelin/contracts/proxy/Clones.sol";
import "./InscriptionToken.sol";

contract InscriptionTokenFactory {
    address public immutable implementation;

    event TokenMinted(
        address indexed proxy,
        address indexed owner,
        string indexed nameSymbol,
        uint256 supply
    );

    constructor() {
        implementation = address(new InscriptionToken());
    }

    function mintInscriptionToken(
        string memory name,
        string memory symbol,
        uint256 initialSupply
    ) external returns (address proxy) {
        //clone constract address
        proxy = Clones.clone(implementation);
        //typecast
        InscriptionToken(proxy).initialize(
            name,
            symbol,
            msg.sender,
            initialSupply
        );
        emit TokenMinted(
            proxy,
            msg.sender,
            string(abi.encodePacked(name, "_", symbol)),
            initialSupply
        );
    }
}

设计

1. 最小代理克隆(Clones)

采用"工厂合约"模式:

  • InscriptionTokenFactory:工厂合约,负责批量克隆和初始化Token实例。
  • InscriptionToken:支持permit离线授权。

每个用户铸造的Token都是独立的合约实例,互不影响。

工厂合约内部维护一个实现合约(implementation),每次mint时通过OpenZeppelin的Clones.clone方法克隆出一个最小代理合约:

address proxy = Clones.clone(implementation);

底层使用的vm create 实现的是随机地址

这样可以极大节省部署gas,并保证所有Token逻辑一致。

2. 初始化

被克隆的Token合约采用Initializable模式,初始化参数通过initialize函数传递,实现合约只可初始化一次,防止重复初始化:

function initialize(
    string memory name_,
    string memory symbol_,
    address initialOwner_,
    uint256 initialSupply_
) public initializer {
    __ERC20_init(name_, symbol_);
    __ERC20Permit_init(name_);
    _mint(initialOwner_, initialSupply_);
}

3. 离线授权(permit)

Token合约继承自ERC20PermitUpgradeable,自动支持EIP-2612 permit功能,用户可通过签名离线授权,无需链上approve,提升用户体验。

4. 事件设计

event TokenMinted(
    address indexed proxy, //topic0
    address indexed owner, //topic1
    string indexed nameSymbol, //topic2
    uint256 supply
);

部署anvil 本地测试节点

forge create src/InscriptionTokenFactory.sol:InscriptionTokenFactory //模拟部署
forge create src/InscriptionTokenFactory.sol:InscriptionTokenFactory ----broadcast //广播

在部署之前进行模拟部署可以获取很多有用信息,比如gas

cast --to-dec 0x267e9d //解析十六进制的gas值
2522781
cast gas-price     //如果需要目标节点 --rpc-url
2000000000         //wei
cast from-wei $((2000000000 * 2523037))
0.005046074000000000

cast format-units $((2000000000 * 2523037)) 18 // 精度1e18
0.005046074000000000

gas非常重要 在测试时也可生成报告

forge test --match-contract InscriptionTokenFactory --gas-report
[⠊] Compiling...
No files changed, compilation skipped

Ran 2 tests for test/InscriptionTokenFactory.t.sol:InscriptionTokenFactoryTest
[PASS] testCannotReinitializeToken() (gas: 287376)
[PASS] testMintInscriptionToken() (gas: 296974)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 11.72ms (4.17ms CPU time)

╭----------------------------------------------------+-----------------+--------+--------+--------+---------╮
| src/InscriptionToken.sol:InscriptionToken Contract |                 |        |        |        |         |
+===========================================================================================================+
| Deployment Cost                                    | Deployment Size |        |        |        |         |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
| 0                                                  | 9459            |        |        |        |         |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
|                                                    |                 |        |        |        |         |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
| Function Name                                      | Min             | Avg    | Median | Max    | # Calls |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
| balanceOf                                          | 2969            | 2969   | 2969   | 2969   | 1       |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
| initialize                                         | 4824            | 115332 | 170586 | 170586 | 3       |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
| name                                               | 3405            | 3405   | 3405   | 3405   | 1       |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
| symbol                                             | 3448            | 3448   | 3448   | 3448   | 1       |
|----------------------------------------------------+-----------------+--------+--------+--------+---------|
| totalSupply                                        | 2552            | 2552   | 2552   | 2552   | 1       |
╰----------------------------------------------------+-----------------+--------+--------+--------+---------╯

╭------------------------------------------------------------------+-----------------+--------+--------+--------+---------╮
| src/InscriptionTokenFactory.sol:InscriptionTokenFactory Contract |                 |        |        |        |         |
+=========================================================================================================================+
| Deployment Cost                                                  | Deployment Size |        |        |        |         |
|------------------------------------------------------------------+-----------------+--------+--------+--------+---------|
| 2522781                                                          | 11472           |        |        |        |         |
|------------------------------------------------------------------+-----------------+--------+--------+--------+---------|
|                                                                  |                 |        |        |        |         |
|------------------------------------------------------------------+-----------------+--------+--------+--------+---------|
| Function Name                                                    | Min             | Avg    | Median | Max    | # Calls |
|------------------------------------------------------------------+-----------------+--------+--------+--------+---------|
| mintInscriptionToken                                             | 243490          | 243526 | 243526 | 243562 | 2       |
╰------------------------------------------------------------------+-----------------+--------+--------+--------+---------╯


Ran 1 test suite in 19.04ms (11.72ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

cast 命令测试

cast estimate 0x5FbDB2315678afecb367f032d93F642f64180aa3 "mintInscriptionToken(string,string,uint256)" "MyToken" "MTK" 1000ether 
246191

cast from-wei $((2000000000 * 246191))
0.000492382000000000  //ether

cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 "mintInscriptionToken(string,string,uint256)" "MyToken" "MTK" 1000ether 

246191

"logs": [
    {
        "address": "0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968",
        "topics": [
            "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", 
            "0x0000000000000000000000000000000000000000000000000000000000000000", 
            "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266" 
        ],
        "data": "0x00000000000000000000000000000000000000000000003635c9adc5dea00000"
    },
    {
        "address": "0xb7a5bd0345ef1cc5e66bf61bdec17d2461fbd968",
        "topics": [
            "0xc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d2" 
        ],
        "data": "0x0000000000000000000000000000000000000000000000000000000000000001"
    },
    {
        "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3",
        "topics": [
            "0xf36e967fc37dc0a7e0a1e6d328aac7e945dfcadb00d552d6bc291bdc1b5f80d3", 
            "0x000000000000000000000000b7a5bd0345ef1cc5e66bf61bdec17d2461fbd968", 
            "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", 
            "0xdf6ee795177053484a469154c5b769a400e7361d56773e0d480595b805f0a5fd"  
        ],
        "data": "0x00000000000000000000000000000000000000000000003635c9adc5dea00000"
    }
]