介绍
随着铭文(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"
}
]