📋 目录
概述
背景
当前 S11e Protocol 设计中,所有品牌共享同一个 AssetFactory 和统一的 Issuer 合约实例。平台账户拥有 AssetFactory 的 owner 权限,可以随时修改 Issuers 配置,影响所有品牌。这导致:
- ❌ 品牌缺乏自主权
- ❌ 平台可以控制所有品牌的资产创建
- ❌ 品牌无法自定义资产发行逻辑
- ❌ 品牌之间缺乏隔离
解决方案
品牌独立 Issuer 方案:
- ✅ 平台部署统一的
AssetFactory(支持 Profile 隔离) - ✅ 品牌创建 Profile 时,自动为每个品牌部署独立的 Issuer 实例
- ✅ 将品牌地址设置为每个 Issuer 的 owner
- ✅ 品牌完全拥有和控制自己的 Issuers
- ✅ 使用 Clone Factory(EIP-1167)降低 Gas 成本
核心优势
- 完全去中心化:品牌拥有自己的 Issuer 实例,owner = 品牌地址
- 品牌自主权:品牌可以部署和配置自定义 Issuers
- 平台无法控制:权限验证确保只有品牌可以设置自己的 Issuers
- Gas 成本合理:~225,000 gas(约 $10-15/品牌,使用 Clone Factory)
- 隔离性强:品牌之间完全隔离,互不影响
设计目标
功能目标
- ✅ 品牌完全拥有 Issuers:每个品牌拥有独立的 Issuer 实例,owner = 品牌地址
- ✅ 品牌自主配置:品牌可以设置、替换自己的 Issuers
- ✅ 平台无法控制:平台无法修改品牌的 Issuer 配置
- ✅ 开箱即用:品牌创建 Profile 后可以立即创建资产
- ✅ 灵活扩展:品牌可以部署自定义 Issuer 合约
技术目标
- ✅ Gas 成本优化:使用 Clone Factory 降低部署成本
- ✅ 向后兼容:可以与现有部署兼容(可选)
- ✅ 实现简单:基于现有合约,改动最小
- ✅ 安全性:完善的权限验证机制
架构设计
整体架构
┌─────────────────────────────────────────────────────────────┐
│ 平台层(部署一次) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ AssetFactory │ │ Issuer Factory │ │
│ │ (统一,支持隔离) │ │ (使用 Clone Factory) │ │
│ └──────────────────┘ └──────────────────────┘ │
│ │ │ │
│ │ profileIssuers │ 创建实例 │
│ │ mapping │ │
│ │ │ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Issuer 模板合约 │ │ 平台默认 Issuers │ │
│ │ - PassCard │ │ (可选,品牌可替换) │ │
│ │ - DigitalPoints │ │ │ │
│ │ - Badge │ │ │ │
│ │ - POAP │ │ │ │
│ │ - Ticket │ │ │ │
│ └──────────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 品牌层(每个品牌独立) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 品牌 A (Profile A) │
│ ┌──────────────┐ │
│ │ S11eProfile │ │
│ │ A │ │
│ └──────┬───────┘ │
│ │ 使用 │
│ ▼ │
│ ┌──────────────┐ │
│ │ AssetFactory │ ──┐ │
│ │ (统一) │ │ │
│ └──────────────┘ │ profileIssuers[A] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 品牌 A 专属 Issuers (owner = A) │ │
│ ├─────────────────────────────────────┤ │
│ │ PassCardIssuer A (Clone) │ │
│ │ DigitalPointsIssuer A (Clone) │ │
│ │ BadgeIssuer A (Clone) │ │
│ │ POAPIssuer A (Clone) │ │
│ │ TicketIssuer A (Clone) │ │
│ └─────────────────────────────────────┘ │
│ │
│ 品牌 B (Profile B) │
│ ┌──────────────┐ │
│ │ S11eProfile │ │
│ │ B │ │
│ └──────┬───────┘ │
│ │ 使用 │
│ ▼ │
│ ┌──────────────┐ │
│ │ AssetFactory │ ──┐ │
│ │ (统一) │ │ profileIssuers[B] │
│ └──────────────┘ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 品牌 B 专属 Issuers (owner = B) │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
数据流
品牌创建 Profile
↓
ProfileFactory.createProfile()
↓
1. 创建 S11eProfile 实例
↓
2. 调用 Issuer Factory 创建 5 个 Issuer 实例(Clone)
- PassCardIssuer.clone() → PassCardIssuer A (owner = 品牌地址)
- DigitalPointsIssuer.clone() → DigitalPointsIssuer A
- BadgeIssuer.clone() → BadgeIssuer A
- POAPIssuer.clone() → POAPIssuer A
- TicketIssuer.clone() → TicketIssuer A
↓
3. 在 AssetFactoryV2 中注册品牌专属 Issuers
- assetFactoryV2.setIssuerForProfile(profile, 1, passCardIssuerA)
- assetFactoryV2.setIssuerForProfile(profile, 2, digitalPointsIssuerA)
- ...
↓
4. 在 Profile 中设置 AssetFactoryV2(可选,如果 Profile 支持)
- profile.setAssetFactoryV2(assetFactoryV2)
↓
品牌发行资产
↓
方式 A: profile.issueAsset()(如果 Profile 已适配 V2)
方式 B: assetFactoryV2.createPassCard(profile, ...)(直接调用)
↓
AssetFactoryV2 查找 profileIssuers[profile][1]
↓
如果未设置 → 使用 defaultIssuers[1](平台默认)
如果已设置 → 使用 profileIssuers[profile][1](品牌专属)
↓
PassCardIssuer A.createPassCard(...)
↓
部署 PassCard 合约并返回地址
技术实现
1. Clone Factory 模式(EIP-1167)
目的:降低部署 Issuer 实例的 Gas 成本
实现:
solidity
import "@openzeppelin/contracts/proxy/Clones.sol";
contract PassCardIssuerFactory {
address public immutable implementation;
constructor(address _implementation) {
implementation = _implementation;
}
function createIssuerForProfile(address profileOwner) external returns (address) {
address issuer = Clones.clone(implementation);
PassCardIssuer(issuer).initialize(profileOwner);
return issuer;
}
}
Gas 成本:
- 直接部署:~100,000+ gas/Issuer
- Clone:~45,000 gas/Issuer
- 节省:~55% Gas 成本
2. Issuer 合约初始化模式
目的:支持 Clone Factory,同时保持模板合约不可直接使用
solidity
contract PassCardIssuer {
address public owner;
bool private initialized;
constructor() {
_disableInitializers(); // 模板合约,禁用初始化
}
function initialize(address _owner) external {
require(!initialized, "Already initialized");
initialized = true;
owner = _owner;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function createPassCard(...) external onlyOwner returns (address) {
PassCard card = new PassCard(...);
return address(card);
}
}
3. AssetFactory Profile 隔离
目的:支持每个品牌独立配置 Issuers
solidity
contract AssetFactoryV2 { // ✅ 注意:这是 AssetFactoryV2,不是 AssetFactory
// Profile 地址 => 资产类型 => Issuer 地址
mapping(address => mapping(uint8 => address)) public profileIssuers;
// 平台默认 Issuers(可选)
mapping(uint8 => address) public defaultIssuers;
address public owner;
address public platformAdmin;
/**
* @notice 品牌设置自己的 Issuer
* @dev 只能由 Profile owner 调用
*/
function setIssuerForProfile(
address profile,
uint8 assetType,
address issuer
) external {
// ✅ 权限验证:只能由 Profile owner 调用
IS11eProfile profileContract = IS11eProfile(profile);
require(
profileContract.hasRole(bytes32(0), msg.sender), // DEFAULT_ADMIN_ROLE = 0x00
"AssetFactory: Only profile owner can set issuer"
);
// 如果 issuer 为 0,使用平台默认
if (issuer == address(0)) {
require(
defaultIssuers[assetType] != address(0),
"AssetFactory: Default issuer not set"
);
profileIssuers[profile][assetType] = defaultIssuers[assetType];
} else {
profileIssuers[profile][assetType] = issuer;
}
emit ProfileIssuerSet(profile, assetType, profileIssuers[profile][assetType]);
}
/**
* @notice 创建 PassCard(使用 Profile 专属 Issuer)
*/
function createPassCard(
address profile, // ✅ 新增 Profile 参数
string memory name,
string memory symbol,
address assetOwner, // ✅ 使用 assetOwner 避免与状态变量 owner 冲突
string memory baseURI,
address erc6551Registry,
uint256 maxSupply
) external returns (address) {
address issuer = profileIssuers[profile][1]; // 使用品牌专属 Issuer
if (issuer == address(0)) {
// 如果品牌未设置,使用平台默认(首次使用时自动设置)
issuer = defaultIssuers[1];
require(issuer != address(0), "PassCard issuer not set");
profileIssuers[profile][1] = issuer; // 自动写入
}
address asset = IPassCardIssuer(issuer).createPassCard(
name, symbol, baseURI, maxSupply, erc6551Registry, assetOwner
);
emit AssetCreated(profile, asset, 1);
return asset;
}
}
合约设计
1. AssetFactoryV2(新版本)
文件 : contracts/core/AssetFactoryV2.sol
状态: ✅ 已创建
主要特性:
- ✅
profileIssuers映射:mapping(address => mapping(uint8 => address))- 每个品牌独立配置 - ✅
setIssuerForProfile()方法:品牌设置自己的 Issuer(权限验证) - ✅
getIssuer()方法:获取 Profile 的 Issuer,自动回退到平台默认(如果未设置) - ✅ 所有
createXxx()方法:添加address profile参数,自动处理 Issuer 查找- 优先使用品牌专属 Issuer
- 如果未设置,自动使用平台默认 Issuer 并写入
profileIssuers(首次使用时)
- ✅
defaultIssuers映射:平台默认 Issuers(品牌可选使用) - ✅
platformAdmin:平台管理员(设置默认 Issuers)
关键接口:
solidity
interface IAssetFactoryV2 {
function setIssuerForProfile(
address profile,
uint8 assetType,
address issuer
) external;
function setDefaultIssuer(uint8 assetType, address issuer) external;
/**
* @notice 获取 Profile 的 Issuer(如果未设置则返回平台默认)
* @dev 这是一个视图函数,不会修改状态
* @param profile Profile 合约地址
* @param assetType 资产类型 (1=PassCard, 2=DigitalPoints, 4=Badge, 5=POAP, 6=Ticket)
* @return Issuer 合约地址(如果品牌未设置且平台默认也未设置,返回 address(0))
*/
function getIssuer(address profile, uint8 assetType) external view returns (address);
function createPassCard(
address profile,
string memory name,
string memory symbol,
address assetOwner, // ✅ 注意:使用 assetOwner 而不是 owner
string memory baseURI,
address erc6551Registry,
uint256 maxSupply
) external returns (address);
// ... 其他 createXxx() 方法类似
}
与旧版兼容性:
- ✅ 旧版
AssetFactory.sol保持不变 - ✅ V2 版本可独立部署
- ✅ 可以逐步迁移现有 Profile 到 V2
2. Issuer Factory(新增)
状态: ✅ 已创建所有 Issuer Factory
文件列表:
contracts/core/issuers/PassCardIssuerFactory.solcontracts/core/issuers/DigitalPointsIssuerFactory.solcontracts/core/issuers/BadgeIssuerFactory.solcontracts/core/issuers/POAPIssuerFactory.solcontracts/core/issuers/TicketIssuerFactory.sol
功能:
- ✅ 使用 EIP-1167 Clone Factory 为品牌部署 Issuer 实例
- ✅ 初始化时将 owner 设置为品牌地址
- ✅ Gas 成本优化(每个 Clone ~45,000 gas vs 直接部署 ~100,000+ gas)
实现示例:
solidity
contract PassCardIssuerFactory {
address public immutable implementation;
event IssuerCreated(
address indexed profile,
address indexed issuer,
address indexed owner
);
constructor(address _implementation) {
require(_implementation != address(0), "Invalid implementation");
implementation = _implementation;
}
function createIssuerForProfile(address profileOwner)
external
returns (address)
{
require(profileOwner != address(0), "Invalid owner");
// 使用 OpenZeppelin Clones 库(EIP-1167)
address issuer = Clones.clone(implementation);
// 初始化,设置 owner
PassCardIssuerV2(issuer).initialize(profileOwner);
emit IssuerCreated(msg.sender, issuer, profileOwner);
return issuer;
}
}
3. Issuer V2 合约(新版本)
状态: ✅ 已创建所有 Issuer V2 版本
文件列表:
contracts/core/issuers/PassCardIssuerV2.solcontracts/core/issuers/DigitalPointsIssuerV2.solcontracts/core/issuers/BadgeIssuerV2.solcontracts/core/issuers/POAPIssuerV2.solcontracts/core/issuers/TicketIssuerV2.sol
主要特性:
- ✅ 使用
Initializable(OpenZeppelin Upgradeable) - ✅
initialize(address _owner)方法:设置 owner - ✅
owner状态变量和onlyOwner修饰符 - ✅ 构造函数中禁用初始化(
_disableInitializers()) - 模板合约 - ✅ 保持原有
createXxx()方法逻辑
实现示例:
solidity
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "../../extensions/profileAssets/PassCard.sol";
contract PassCardIssuerV2 is Initializable {
address public owner;
event PassCardCreated(address indexed issuer, address indexed passCard, address indexed owner);
constructor() {
_disableInitializers(); // 模板合约,禁用直接初始化
}
function initialize(address _owner) external initializer {
require(_owner != address(0), "Invalid owner");
owner = _owner;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function createPassCard(...) external onlyOwner returns (address) {
PassCard card = new PassCard(...);
emit PassCardCreated(msg.sender, address(card), assetOwner);
return address(card);
}
// 兼容接口
function issueAsset(bytes memory params) external onlyOwner returns (address) {
// ... 解析参数并调用 createPassCard
}
}
与旧版兼容性:
- ✅ 旧版 Issuer 合约保持不变
- ✅ V2 版本使用不同的命名,不会冲突
- ✅ 可以并行使用(旧版用于现有部署,V2 用于新部署)
4. S11eProfile(需要适配 V2)
文件 : contracts/core/S11eProfile.sol
适配方案(推荐创建 S11eProfileV2 或添加适配层):
方案 A: 创建适配器合约(推荐)
solidity
// contracts/core/adapters/AssetFactoryAdapter.sol
contract AssetFactoryAdapter {
IAssetFactoryV2 public assetFactoryV2;
constructor(address _assetFactoryV2) {
assetFactoryV2 = IAssetFactoryV2(_assetFactoryV2);
}
// 兼容旧接口,内部调用 V2
function createPassCard(
string memory name,
string memory symbol,
address assetOwner, // ✅ 注意:使用 assetOwner
string memory baseURI,
address erc6551Registry,
uint256 maxSupply
) external returns (address) {
// 从 msg.sender 推断 Profile 地址(需要 Profile 传递)
// 或使用适配器模式
return assetFactoryV2.createPassCard(
msg.sender, // 假设 msg.sender 是 Profile 地址
name, symbol, assetOwner, baseURI, erc6551Registry, maxSupply
);
}
}
方案 B: 修改 S11eProfile 支持 V2(如果兼容性允许)
主要修改点:
- 修改
issueAsset()方法,传递address(this)给 AssetFactoryV2 - 添加类型检查(判断是 AssetFactory 还是 AssetFactoryV2)
- 添加
setIssuer()和setIssuers()便捷方法(调用 V2)
solidity
contract S11eProfile {
IAssetFactory public assetFactory; // 兼容旧版
IAssetFactoryV2 public assetFactoryV2; // 新版
/**
* @notice 设置 AssetFactory V2
*/
function setAssetFactoryV2(address _assetFactoryV2) external onlyRole(DEFAULT_ADMIN_ROLE) {
require(_assetFactoryV2 != address(0), "Invalid factory address");
assetFactoryV2 = IAssetFactoryV2(_assetFactoryV2);
}
function issueAsset(AssetsStruct memory _assetsInfo) external returns (address) {
require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "must have owner role");
uint8 assetType = _assetsInfo.assetsType;
address assetsAddress_;
// ✅ 如果使用 V2,传递 address(this)
if (address(assetFactoryV2) != address(0)) {
if (assetType == 1) { // PASSCARD
assetsAddress_ = assetFactoryV2.createPassCard(
address(this), // ✅ 传递 Profile 地址
_assetsInfo.name,
_assetsInfo.symbol,
_msgSender(),
baseURI,
address(erc6551Registry),
_assetsInfo.supply
);
} else if (assetType == 2) { // DP
assetsAddress_ = assetFactoryV2.createDigitalPoints(
address(this),
_assetsInfo.name,
_assetsInfo.symbol,
_assetsInfo.supply,
_msgSender()
);
}
// ... 其他资产类型
} else {
// 向后兼容:使用旧版 AssetFactory
// ... 原有逻辑
}
// 记录资产
// ...
}
/**
* @notice 设置资产发行器(便捷方法,用于 V2)
*/
function setIssuer(uint8 assetType, address issuer)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
require(address(assetFactoryV2) != address(0), "AssetFactoryV2 not set");
assetFactoryV2.setIssuerForProfile(
address(this),
assetType,
issuer
);
emit IssuerSet(assetType, issuer);
}
/**
* @notice 批量设置资产发行器
*/
function setIssuers(uint8[] memory assetTypes, address[] memory issuers)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
require(assetTypes.length == issuers.length, "Array length mismatch");
require(address(assetFactoryV2) != address(0), "AssetFactoryV2 not set");
for (uint256 i = 0; i < assetTypes.length; i++) {
assetFactoryV2.setIssuerForProfile(address(this), assetTypes[i], issuers[i]);
emit IssuerSet(assetTypes[i], issuers[i]);
}
}
event IssuerSet(uint8 indexed assetType, address indexed issuer);
}
5. S11eProfileFactory(需要创建 V2 版本)
建议 : 创建 S11eProfileFactoryV2.sol 支持自动部署 Issuers
文件 : contracts/core/S11eProfileFactoryV2.sol(待创建)
功能设计:
- ✅ 支持自动部署品牌专属 Issuers(通过 Issuer Factory)
- ✅ 自动在 AssetFactoryV2 中注册 Issuers
- ✅ 自动设置 Profile 的 AssetFactoryV2
- ✅ 可选:支持使用平台默认 Issuers(跳过部署,直接配置)
实现示例:
solidity
contract S11eProfileFactoryV2 {
PassCardIssuerFactory public passCardIssuerFactory;
DigitalPointsIssuerFactory public digitalPointsIssuerFactory;
BadgeIssuerFactory public badgeIssuerFactory;
POAPIssuerFactory public poapIssuerFactory;
TicketIssuerFactory public ticketIssuerFactory;
AssetFactoryV2 public assetFactoryV2;
S11eProfileFactory public profileFactory; // 复用现有 ProfileFactory
event ProfileWithIssuersCreated(
address indexed profile,
address indexed owner,
address[5] indexed issuers // [PassCard, DigitalPoints, Badge, POAP, Ticket]
);
/**
* @notice 创建 Profile 并自动部署 Issuers
*/
function createProfileWithIssuers(
IS11eProfile.ProfileStruct memory _profileStruct,
address owner,
bool usePlatformDefaults // 是否使用平台默认(跳过部署)
) external returns (address) {
// 1. 创建 Profile(使用现有 Factory)
address profileAddress = profileFactory.createProfile(_profileStruct, owner);
IS11eProfile profile = IS11eProfile(profileAddress);
address[5] memory issuers;
if (usePlatformDefaults) {
// 使用平台默认 Issuers(不部署新实例)
// 直接从 AssetFactoryV2 读取默认值
issuers[0] = assetFactoryV2.defaultIssuers(1); // PassCard
issuers[1] = assetFactoryV2.defaultIssuers(2); // DigitalPoints
issuers[2] = assetFactoryV2.defaultIssuers(4); // Badge
issuers[3] = assetFactoryV2.defaultIssuers(5); // POAP
issuers[4] = assetFactoryV2.defaultIssuers(6); // Ticket
} else {
// 2. 为品牌部署所有 Issuer 实例(Clone)
issuers[0] = passCardIssuerFactory.createIssuerForProfile(owner);
issuers[1] = digitalPointsIssuerFactory.createIssuerForProfile(owner);
issuers[2] = badgeIssuerFactory.createIssuerForProfile(owner);
issuers[3] = poapIssuerFactory.createIssuerForProfile(owner);
issuers[4] = ticketIssuerFactory.createIssuerForProfile(owner);
}
// 3. 在 AssetFactoryV2 中注册品牌专属 Issuers
// ⚠️ 注意:setIssuerForProfile 只能由 Profile owner 调用
// 如果 Factory 内部调用,需要使用 owner 的签名或权限委托
// 实际实现中,可能需要 Factory 持有临时权限或使用委托调用
// 示例(需要实际权限):
// assetFactoryV2.setIssuerForProfile(profileAddress, 1, issuers[0]);
// 或通过 owner 授权后调用
// 4. 在 Profile 中设置 AssetFactoryV2(如果 Profile 支持)
// profile.setAssetFactoryV2(address(assetFactoryV2));
emit ProfileWithIssuersCreated(profileAddress, owner, issuers);
return profileAddress;
}
}
注意事项:
- ⚠️ 需要在 Profile 中设置 AssetFactoryV2,可能需要修改 Profile 或使用适配器
- ⚠️ 部署 Issuers 需要 Profile owner 授权,可能需要调整权限逻辑
- ⚠️
setIssuerForProfile()只能由 Profile owner 调用,Factory 内部调用时需要注意权限传递
权限控制
权限矩阵
| 操作 | 调用者 | 权限验证 | 说明 |
|---|---|---|---|
AssetFactory.setIssuerForProfile() |
Profile owner | ✅ 验证 hasRole(DEFAULT_ADMIN_ROLE) |
只有品牌可以设置自己的 Issuers |
AssetFactory.createXxx() |
任意 | ⚠️ 使用 Profile 专属 Issuer | Profile owner 间接控制 |
Issuer.createXxx() |
Issuer owner | ✅ onlyOwner |
只有 Issuer owner(品牌)可以调用 |
Profile.setIssuer() |
Profile owner | ✅ onlyRole(DEFAULT_ADMIN_ROLE) |
品牌便捷方法 |
安全保证
-
平台无法控制品牌配置
solidity// AssetFactory.setIssuerForProfile() require( profileContract.hasRole(DEFAULT_ADMIN_ROLE, msg.sender), "Only profile owner can set issuer" );- ✅ 平台账户不是 Profile 的 owner,无法调用
-
品牌只能设置自己的配置
- ✅
profileIssuers[profile][assetType]映射确保品牌之间隔离 - ✅ 品牌只能修改自己的映射,无法影响其他品牌
- ✅
-
Issuer owner = 品牌地址
- ✅ 品牌拥有 Issuer 实例的 owner 权限
- ✅ 品牌可以完全控制 Issuer 的行为
部署流程
💡 提示 :完整的部署脚本已创建,位于
scripts/deploy-v2-platform.js,可以直接使用。
阶段 1: 平台部署(一次性)
使用现有脚本(推荐):
bash
npx hardhat run scripts/deploy-v2-platform.js --network sepolia
或手动部署(参考脚本):
javascript
const { ethers } = require("hardhat");
async function deployV2Platform() {
const [deployer, platformAdmin] = await ethers.getSigners();
console.log('🚀 开始部署 V2 平台合约...\n');
// ==========================================
// 1. 部署 Issuer V2 模板合约
// ==========================================
console.log('📦 部署 Issuer V2 模板合约...');
const PassCardIssuerV2 = await ethers.getContractFactory('PassCardIssuerV2');
const passCardIssuerTemplate = await PassCardIssuerV2.deploy();
await passCardIssuerTemplate.waitForDeployment();
console.log(' ✅ PassCardIssuerV2:', await passCardIssuerTemplate.getAddress());
const DigitalPointsIssuerV2 = await ethers.getContractFactory('DigitalPointsIssuerV2');
const digitalPointsIssuerTemplate = await DigitalPointsIssuerV2.deploy();
await digitalPointsIssuerTemplate.waitForDeployment();
console.log(' ✅ DigitalPointsIssuerV2:', await digitalPointsIssuerTemplate.getAddress());
const BadgeIssuerV2 = await ethers.getContractFactory('BadgeIssuerV2');
const badgeIssuerTemplate = await BadgeIssuerV2.deploy();
await badgeIssuerTemplate.waitForDeployment();
console.log(' ✅ BadgeIssuerV2:', await badgeIssuerTemplate.getAddress());
const POAPIssuerV2 = await ethers.getContractFactory('POAPIssuerV2');
const poapIssuerTemplate = await POAPIssuerV2.deploy();
await poapIssuerTemplate.waitForDeployment();
console.log(' ✅ POAPIssuerV2:', await poapIssuerTemplate.getAddress());
const TicketIssuerV2 = await ethers.getContractFactory('TicketIssuerV2');
const ticketIssuerTemplate = await TicketIssuerV2.deploy();
await ticketIssuerTemplate.waitForDeployment();
console.log(' ✅ TicketIssuerV2:', await ticketIssuerTemplate.getAddress());
console.log('');
// ==========================================
// 2. 部署 Issuer Factory
// ==========================================
console.log('🏭 部署 Issuer Factory...');
const PassCardIssuerFactory = await ethers.getContractFactory('PassCardIssuerFactory');
const passCardIssuerFactory = await PassCardIssuerFactory.deploy(
await passCardIssuerTemplate.getAddress()
);
await passCardIssuerFactory.waitForDeployment();
console.log(' ✅ PassCardIssuerFactory:', await passCardIssuerFactory.getAddress());
const DigitalPointsIssuerFactory = await ethers.getContractFactory('DigitalPointsIssuerFactory');
const digitalPointsIssuerFactory = await DigitalPointsIssuerFactory.deploy(
await digitalPointsIssuerTemplate.getAddress()
);
await digitalPointsIssuerFactory.waitForDeployment();
console.log(' ✅ DigitalPointsIssuerFactory:', await digitalPointsIssuerFactory.getAddress());
const BadgeIssuerFactory = await ethers.getContractFactory('BadgeIssuerFactory');
const badgeIssuerFactory = await BadgeIssuerFactory.deploy(
await badgeIssuerTemplate.getAddress()
);
await badgeIssuerFactory.waitForDeployment();
console.log(' ✅ BadgeIssuerFactory:', await badgeIssuerFactory.getAddress());
const POAPIssuerFactory = await ethers.getContractFactory('POAPIssuerFactory');
const poapIssuerFactory = await POAPIssuerFactory.deploy(
await poapIssuerTemplate.getAddress()
);
await poapIssuerFactory.waitForDeployment();
console.log(' ✅ POAPIssuerFactory:', await poapIssuerFactory.getAddress());
const TicketIssuerFactory = await ethers.getContractFactory('TicketIssuerFactory');
const ticketIssuerFactory = await TicketIssuerFactory.deploy(
await ticketIssuerTemplate.getAddress()
);
await ticketIssuerFactory.waitForDeployment();
console.log(' ✅ TicketIssuerFactory:', await ticketIssuerFactory.getAddress());
console.log('');
// ==========================================
// 3. 部署 AssetFactoryV2
// ==========================================
console.log('🏭 部署 AssetFactoryV2...');
const AssetFactoryV2 = await ethers.getContractFactory('AssetFactoryV2');
const assetFactoryV2 = await AssetFactoryV2.deploy(platformAdmin.address);
await assetFactoryV2.waitForDeployment();
console.log(' ✅ AssetFactoryV2:', await assetFactoryV2.getAddress());
console.log(' ℹ️ 注意: AssetFactoryV2 构造函数需要 platformAdmin 地址');
console.log('');
// ==========================================
// 4. 设置平台默认 Issuers(可选)
// ==========================================
console.log('⚙️ 配置平台默认 Issuers...');
// 平台可以选择使用已部署的旧版 Issuers 或新部署 V2 模板作为默认
// 这里使用 V2 模板(实际应该使用已部署的实例)
// 如果需要,可以部署一组平台默认 Issuer 实例
const platformPassCardIssuer = await passCardIssuerFactory.createIssuerForProfile(platformAdmin.address);
const platformDigitalPointsIssuer = await digitalPointsIssuerFactory.createIssuerForProfile(platformAdmin.address);
const platformBadgeIssuer = await badgeIssuerFactory.createIssuerForProfile(platformAdmin.address);
const platformPOAPIssuer = await poapIssuerFactory.createIssuerForProfile(platformAdmin.address);
const platformTicketIssuer = await ticketIssuerFactory.createIssuerForProfile(platformAdmin.address);
await assetFactoryV2.connect(platformAdmin).setDefaultIssuer(1, platformPassCardIssuer);
await assetFactoryV2.connect(platformAdmin).setDefaultIssuer(2, platformDigitalPointsIssuer);
await assetFactoryV2.connect(platformAdmin).setDefaultIssuer(4, platformBadgeIssuer);
await assetFactoryV2.connect(platformAdmin).setDefaultIssuer(5, platformPOAPIssuer);
await assetFactoryV2.connect(platformAdmin).setDefaultIssuer(6, platformTicketIssuer);
console.log(' ✅ 平台默认 Issuers 已配置\n');
// ==========================================
// 5. 保存部署地址
// ==========================================
const deployment = {
network: hre.network.name,
chainId: (await ethers.provider.getNetwork()).chainId.toString(),
timestamp: new Date().toISOString(),
contracts: {
assetFactoryV2: await assetFactoryV2.getAddress(),
issuers: {
passCardTemplate: await passCardIssuerTemplate.getAddress(),
digitalPointsTemplate: await digitalPointsIssuerTemplate.getAddress(),
badgeTemplate: await badgeIssuerTemplate.getAddress(),
poapTemplate: await poapIssuerTemplate.getAddress(),
ticketTemplate: await ticketIssuerTemplate.getAddress(),
},
factories: {
passCardFactory: await passCardIssuerFactory.getAddress(),
digitalPointsFactory: await digitalPointsIssuerFactory.getAddress(),
badgeFactory: await badgeIssuerFactory.getAddress(),
poapFactory: await poapIssuerFactory.getAddress(),
ticketFactory: await ticketIssuerFactory.getAddress(),
},
platformDefaults: {
passCard: platformPassCardIssuer,
digitalPoints: platformDigitalPointsIssuer,
badge: platformBadgeIssuer,
poap: platformPOAPIssuer,
ticket: platformTicketIssuer,
}
}
};
const fs = require('fs');
const path = require('path');
const deploymentsDir = path.join(__dirname, '../deployments');
if (!fs.existsSync(deploymentsDir)) {
fs.mkdirSync(deploymentsDir, { recursive: true });
}
const filename = `${hre.network.name}-v2-${Date.now()}.json`;
fs.writeFileSync(
path.join(deploymentsDir, filename),
JSON.stringify(deployment, null, 2)
);
console.log(`✅ 部署信息已保存: deployments/${filename}\n`);
console.log('🎉 V2 平台部署完成!\n');
return deployment;
}
module.exports = { deployV2Platform };
阶段 2: 品牌创建 Profile(自动部署 Issuers)
方式 A: 使用 ProfileFactoryV2(推荐,待实现)
javascript
const profileStruct = {
profileType: 'BRAND',
name: 'My Brand',
symbol: 'MB',
memberNo: 0,
owner: brandOwner.address,
baseURI: 'https://ipfs.s11e.io/',
erc6551Registry: erc6551RegistryAddress,
externalUri: 'https://mybrand.com'
};
// 使用 V2 Factory,自动部署 Issuers
const tx = await profileFactoryV2.createProfileWithIssuers(
profileStruct,
brandOwner.address,
false // 不使用平台默认,部署专属 Issuers
);
const receipt = await tx.wait();
// 自动执行:
// 1. 创建 Profile
// 2. 部署 5 个 Issuer 实例(Clone,owner = brandOwner.address)
// 3. 在 AssetFactoryV2 中注册
// 4. 在 Profile 中设置 AssetFactoryV2
方式 B: 手动流程(当前可用)
javascript
// 1. 创建 Profile(使用现有 Factory)
const profileStruct = { /* ... */ };
const profileTx = await profileFactory.createProfile(profileStruct, brandOwner.address);
const profileReceipt = await profileTx.wait();
// ✅ 从事件或返回值获取 Profile 地址(见上面的完整示例)
const profileAddress = /* ... */; // 使用上面的方法获取
const profile = await ethers.getContractAt('S11eProfile', profileAddress);
// 2. 部署品牌专属 Issuers
const passCardIssuer = await passCardIssuerFactory.createIssuerForProfile(brandOwner.address);
const digitalPointsIssuer = await digitalPointsIssuerFactory.createIssuerForProfile(brandOwner.address);
const badgeIssuer = await badgeIssuerFactory.createIssuerForProfile(brandOwner.address);
const poapIssuer = await poapIssuerFactory.createIssuerForProfile(brandOwner.address);
const ticketIssuer = await ticketIssuerFactory.createIssuerForProfile(brandOwner.address);
// 3. 在 AssetFactoryV2 中注册(需要 Profile owner 调用)
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 1, passCardIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 2, digitalPointsIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 4, badgeIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 5, poapIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 6, ticketIssuer);
// 4. 设置 AssetFactoryV2(如果 Profile 支持)
// ⚠️ 注意:当前 S11eProfile 不支持 setAssetFactoryV2()
// 需要使用方式 2 直接调用 AssetFactoryV2 创建资产
// await profile.connect(brandOwner).setAssetFactoryV2(await assetFactoryV2.getAddress());
Gas 成本估算:
- Profile 创建(Clone):~500,000 gas
- 5 个 Issuer Clone:~225,000 gas(5 × 45,000)
- 注册配置(5 次 setIssuerForProfile):~300,000 gas(5 × 60,000)
- 总计:~1,025,000 gas(按 20 gwei ≈ $45-60)
节省成本:
- 使用平台默认 Issuers(不部署新实例):节省 ~225,000 gas
- 总成本约:~800,000 gas(按 20 gwei ≈ $35-50)
使用示例
⚠️ 重要提示:
- 当前 S11eProfile 合约尚未适配 AssetFactoryV2
- 如果 Profile 未适配,需要使用"方式 2"直接调用 AssetFactoryV2
- 创建资产后,需要手动在 Profile 中注册(使用
register()方法)
1. 品牌创建 Profile 并部署专属 Issuers
javascript
const { ethers } = require("hardhat");
async function createBrandProfile() {
const [deployer, brandOwner] = await ethers.getSigners();
// 读取部署地址
const deployment = require('../deployments/sepolia-v2-latest.json');
const assetFactoryV2 = await ethers.getContractAt(
'AssetFactoryV2',
deployment.contracts.assetFactoryV2
);
const passCardIssuerFactory = await ethers.getContractAt(
'PassCardIssuerFactory',
deployment.contracts.factories.passCardFactory
);
// ... 其他 Factory
// 1. 创建 Profile(使用现有 Factory)
const profileFactory = await ethers.getContractAt('S11eProfileFactory', /* ... */);
const profileStruct = {
profileType: 'BRAND',
name: 'My Brand',
symbol: 'MB',
memberNo: 0,
owner: brandOwner.address,
baseURI: 'https://ipfs.s11e.io/',
erc6551Registry: erc6551RegistryAddress,
externalUri: 'https://mybrand.com'
};
const profileTx = await profileFactory.createProfile(profileStruct, brandOwner.address);
const profileReceipt = await profileTx.wait();
// ✅ 从事件中获取 Profile 地址
let profileAddress;
try {
// 方法 1: 从 ProfileCreated 事件解析(如果通过 S11eCore 创建)
const profileCreatedEvent = profileReceipt.logs.find(log => {
try {
const parsed = s11eCore.interface.parseLog(log);
return parsed && parsed.name === 'ProfileCreated';
} catch { return false; }
});
if (profileCreatedEvent) {
const parsed = s11eCore.interface.parseLog(profileCreatedEvent);
profileAddress = parsed.args.profileAddress;
}
} catch (e) {
console.log('⚠️ 无法从事件解析,尝试其他方法');
}
// 方法 2: 从 S11eCore 查询(如果方法 1 失败)
if (!profileAddress) {
const profileCount = await s11eCore.profileCount();
if (profileCount > 0n) {
profileAddress = await s11eCore.profileAddresses(profileCount - 1n);
}
}
// 方法 3: 直接从返回值获取(createProfile 返回地址)
if (!profileAddress) {
const result = await profileTx.wait();
profileAddress = result.to; // 或从交易结果解析
}
require(profileAddress && profileAddress !== ethers.ZeroAddress, 'Failed to get profile address');
console.log('✅ Profile 创建成功:', profileAddress);
// 2. 部署品牌专属 Issuers
console.log('📦 部署品牌专属 Issuers...');
const passCardIssuer = await passCardIssuerFactory.createIssuerForProfile(brandOwner.address);
const digitalPointsIssuer = await digitalPointsIssuerFactory.createIssuerForProfile(brandOwner.address);
const badgeIssuer = await badgeIssuerFactory.createIssuerForProfile(brandOwner.address);
const poapIssuer = await poapIssuerFactory.createIssuerForProfile(brandOwner.address);
const ticketIssuer = await ticketIssuerFactory.createIssuerForProfile(brandOwner.address);
console.log(' ✅ PassCardIssuer:', passCardIssuer);
console.log(' ✅ DigitalPointsIssuer:', digitalPointsIssuer);
console.log(' ✅ BadgeIssuer:', badgeIssuer);
console.log(' ✅ POAPIssuer:', poapIssuer);
console.log(' ✅ TicketIssuer:', ticketIssuer);
// 3. 注册到 AssetFactoryV2(品牌 owner 调用)
console.log('🔗 在 AssetFactoryV2 中注册 Issuers...');
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 1, passCardIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 2, digitalPointsIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 4, badgeIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 5, poapIssuer);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 6, ticketIssuer);
console.log('✅ Issuers 注册完成\n');
return { profileAddress, issuers: {
passCard: passCardIssuer,
digitalPoints: digitalPointsIssuer,
badge: badgeIssuer,
poap: poapIssuer,
ticket: ticketIssuer
}};
}
2. 品牌查看自己的 Issuers
javascript
async function checkBrandIssuers(profileAddress) {
const assetFactoryV2 = await ethers.getContractAt('AssetFactoryV2', assetFactoryV2Address);
const profile = await ethers.getContractAt('S11eProfile', profileAddress);
console.log('📊 品牌 Issuers 配置:\n');
// 查看各资产类型的 Issuer
const assetTypes = [
{ type: 1, name: 'PassCard' },
{ type: 2, name: 'DigitalPoints' },
{ type: 4, name: 'Badge' },
{ type: 5, name: 'POAP' },
{ type: 6, name: 'Ticket' }
];
for (const { type, name } of assetTypes) {
// ✅ 使用 getIssuer() 方法(自动处理默认值)
const issuerAddress = await assetFactoryV2.getIssuer(profileAddress, type);
if (issuerAddress !== ethers.ZeroAddress) {
console.log(` ${name} (${type}):`, issuerAddress);
// 验证是否为品牌专属 Issuer
const profileIssuer = await assetFactoryV2.profileIssuers(profileAddress, type);
if (profileIssuer !== ethers.ZeroAddress) {
console.log(` 类型: 品牌专属`);
} else {
console.log(` 类型: 平台默认`);
}
// 验证 owner(如果是品牌专属)
if (profileIssuer !== ethers.ZeroAddress) {
try {
const issuer = await ethers.getContractAt(`${name}IssuerV2`, issuerAddress);
const owner = await issuer.owner();
console.log(` Owner: ${owner}`);
} catch (e) {
console.log(` (无法验证 owner)`);
}
}
} else {
console.log(` ${name} (${type}): 未配置`);
}
}
}
3. 品牌替换 Issuer(部署自定义 Issuer)
javascript
async function replaceIssuer(profileAddress, assetType, customIssuerAddress) {
const assetFactoryV2 = await ethers.getContractAt('AssetFactoryV2', assetFactoryV2Address);
const [brandOwner] = await ethers.getSigners();
console.log(`🔄 替换资产类型 ${assetType} 的 Issuer...`);
// 品牌设置自己的 Issuer(只能由 Profile owner 调用)
const tx = await assetFactoryV2.connect(brandOwner).setIssuerForProfile(
profileAddress,
assetType,
customIssuerAddress
);
await tx.wait();
console.log(`✅ Issuer 已替换为: ${customIssuerAddress}`);
// 验证
const issuer = await assetFactoryV2.profileIssuers(profileAddress, assetType);
console.log(` 验证: ${issuer === customIssuerAddress ? '✅ 成功' : '❌ 失败'}`);
}
// 使用示例:部署自定义 PassCardIssuer
async function deployCustomPassCardIssuer() {
const CustomPassCardIssuer = await ethers.getContractFactory('CustomPassCardIssuer');
const customIssuer = await CustomPassCardIssuer.deploy();
await customIssuer.waitForDeployment();
return await customIssuer.getAddress();
}
// 替换
const customIssuerAddress = await deployCustomPassCardIssuer();
await replaceIssuer(profileAddress, 1, customIssuerAddress); // 1 = PassCard
4. 品牌使用平台默认 Issuers(节省 Gas)
javascript
async function usePlatformDefaults(profileAddress) {
const assetFactoryV2 = await ethers.getContractAt('AssetFactoryV2', assetFactoryV2Address);
const [brandOwner] = await ethers.getSigners();
console.log('💰 使用平台默认 Issuers(节省 Gas)...\n');
// 设置 address(0) 表示使用平台默认
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 1, ethers.ZeroAddress);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 2, ethers.ZeroAddress);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 4, ethers.ZeroAddress);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 5, ethers.ZeroAddress);
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 6, ethers.ZeroAddress);
console.log('✅ 已配置使用平台默认 Issuers');
console.log(' Gas 节省:~225,000 gas(未部署专属 Issuers)\n');
}
5. 品牌创建资产(使用专属 Issuer)
javascript
async function issueAssetWithV2(profileAddress) {
const profile = await ethers.getContractAt('S11eProfile', profileAddress);
const assetFactoryV2 = await ethers.getContractAt('AssetFactoryV2', assetFactoryV2Address);
const [brandOwner] = await ethers.getSigners();
// ⚠️ 注意:需要 Profile 支持 AssetFactoryV2
// 如果 Profile 还未支持,需要直接调用 AssetFactoryV2
const assetsInfo = {
protocol: 'ERC721',
assetsType: 1, // PassCard
contractAddress: ethers.ZeroAddress,
name: 'VIP Card',
symbol: 'VIP',
supply: 1000,
externalUri: 'https://mybrand.com/vip'
};
// ⚠️ 方式 1: 如果 Profile 已适配 V2(当前不可用)
// const tx = await profile.connect(brandOwner).issueAsset(assetsInfo);
// ✅ 方式 2: 直接调用 AssetFactoryV2(当前推荐方式)
const tx = await assetFactoryV2.connect(brandOwner).createPassCard(
profileAddress, // Profile 地址
assetsInfo.name,
assetsInfo.symbol,
brandOwner.address, // assetOwner
await profile.baseURI(), // ✅ baseURI 是状态变量,自动生成 getter
await profile.erc6551Registry(), // ✅ erc6551Registry 也是状态变量
assetsInfo.supply
);
const receipt = await tx.wait();
console.log('✅ PassCard 创建成功');
// ✅ 从事件中获取资产地址
const event = receipt.logs.find(log => {
try {
const parsed = assetFactoryV2.interface.parseLog(log);
return parsed && parsed.name === 'AssetCreated';
} catch { return false; }
});
let passCardAddress;
if (event) {
const parsed = assetFactoryV2.interface.parseLog(event);
passCardAddress = parsed.args.asset;
console.log(' PassCard 地址:', passCardAddress);
}
// ⚠️ 重要:如果 Profile 未适配 V2,需要手动注册资产到 Profile
// await profile.connect(brandOwner).register({
// protocol: 'ERC721',
// assetsType: 1,
// contractAddress: passCardAddress,
// name: assetsInfo.name,
// symbol: assetsInfo.symbol,
// supply: assetsInfo.supply,
// externalUri: assetsInfo.externalUri
// });
}
6. 批量设置 Issuers
javascript
async function batchSetIssuers(profileAddress, issuers) {
const assetFactoryV2 = await ethers.getContractAt('AssetFactoryV2', assetFactoryV2Address);
const [brandOwner] = await ethers.getSigners();
const assetTypes = [1, 2, 4, 5, 6]; // PassCard, DigitalPoints, Badge, POAP, Ticket
console.log('🔄 批量设置 Issuers...');
for (let i = 0; i < assetTypes.length; i++) {
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(
profileAddress,
assetTypes[i],
issuers[i]
);
}
console.log('✅ 批量设置完成');
}
// 使用
const issuers = [
passCardIssuerAddress,
digitalPointsIssuerAddress,
badgeIssuerAddress,
poapIssuerAddress,
ticketIssuerAddress
];
await batchSetIssuers(profileAddress, issuers);
实施步骤
阶段 1: 合约开发
状态: ✅ 已完成核心合约
-
✅ AssetFactoryV2.sol - 已创建
- ✅
profileIssuers映射 - ✅
setIssuerForProfile()方法(权限验证) - ✅ 所有
createXxx()方法支持 Profile 参数 - ✅
defaultIssuers映射和setDefaultIssuer()方法
- ✅
-
✅ Issuer V2 合约 - 已创建
- ✅
PassCardIssuerV2.sol - ✅
DigitalPointsIssuerV2.sol - ✅
BadgeIssuerV2.sol - ✅
POAPIssuerV2.sol - ✅
TicketIssuerV2.sol - ✅ 全部支持
initialize()和owner权限
- ✅
-
✅ Issuer Factory 合约 - 已创建
- ✅
PassCardIssuerFactory.sol - ✅
DigitalPointsIssuerFactory.sol - ✅
BadgeIssuerFactory.sol - ✅
POAPIssuerFactory.sol - ✅
TicketIssuerFactory.sol - ✅ 全部使用 EIP-1167 Clone Factory
- ✅
-
⏳ S11eProfile 适配 - 待实现
- 方案 A: 创建
S11eProfileV2.sol(推荐) - 方案 B: 在现有 Profile 中添加 V2 支持
- 需要修改
issueAsset()传递address(this) - 需要添加
setIssuer()和setIssuers()方法
- 方案 A: 创建
-
⏳ S11eProfileFactoryV2 - 待创建
- 自动部署品牌专属 Issuers
- 自动在 AssetFactoryV2 中注册
- 可选:支持使用平台默认(节省 Gas)
阶段 2: 测试(1 周)
-
✅ 单元测试
- Issuer Factory 部署测试
- AssetFactory 权限验证测试
- Profile 创建流程测试
-
✅ 集成测试
- 完整 Profile 创建流程
- 品牌替换 Issuer 测试
- 品牌创建资产测试
-
✅ 安全测试
- 权限控制验证
- 平台无法控制品牌配置
- 品牌之间隔离验证
阶段 3: 部署(1 周)
-
✅ 部署到测试网
- Sepolia 测试网部署
- 完整功能验证
-
✅ 主网部署
- 审计(如果需要)
- 主网部署
- 配置和验证
阶段 4: 迁移(可选)
- ✅ 现有 Profile 迁移
- 为现有 Profile 部署 Issuers
- 更新 AssetFactory 配置
迁移方案
场景 1: 新部署 V2 版本(推荐)
适用于:全新部署或愿意迁移的场景
优势:
- ✅ 不影响现有部署
- ✅ 可以并行运行(旧版和 V2)
- ✅ 逐步迁移策略
步骤:
- ✅ 部署
AssetFactoryV2(已完成) - ✅ 部署所有 Issuer V2 模板和 Factory(已完成)
- ⏳ 创建
S11eProfileFactoryV2(待实现) - 现有 Profile 迁移:
- 可选:为现有 Profile 部署 V2 Issuers
- 可选:在 AssetFactoryV2 中注册
- 可选:更新 Profile 使用 AssetFactoryV2
迁移脚本示例:
javascript
async function migrateProfileToV2(profileAddress) {
const profile = await ethers.getContractAt('S11eProfile', profileAddress);
const [brandOwner] = await ethers.getSigners();
// 1. 为现有 Profile 部署 V2 Issuers
const passCardIssuer = await passCardIssuerFactory.createIssuerForProfile(brandOwner.address);
// ... 其他 Issuers
// 2. 在 AssetFactoryV2 中注册
await assetFactoryV2.connect(brandOwner).setIssuerForProfile(profileAddress, 1, passCardIssuer);
// ...
// 3. 可选:更新 Profile 使用 AssetFactoryV2(如果 Profile 支持)
// await profile.connect(brandOwner).setAssetFactoryV2(await assetFactoryV2.getAddress());
}
场景 2: 混合部署(渐进式迁移)
适用于:现有生产环境,需要渐进式迁移
策略:
- 新品牌使用 V2(自动部署专属 Issuers)
- 现有品牌继续使用旧版,可选择性迁移
实现:
- 部署 V2 版本(并行运行)
- ProfileFactory 检查版本标志
- 新 Profile 自动使用 V2
- 现有 Profile 保持不变或手动迁移
场景 3: 完全迁移
适用于:所有品牌都需要迁移的场景
步骤:
- 为所有现有 Profile 部署 V2 Issuers
- 在 AssetFactoryV2 中注册所有配置
- 更新所有 Profile 使用 AssetFactoryV2
- 停用旧版 AssetFactory
测试策略
1. 单元测试
javascript
describe('PassCardIssuerFactory', function () {
it('应该能够使用 Clone Factory 部署 Issuer', async function () {
const issuer = await factory.createIssuerForProfile(brandOwner.address);
expect(issuer).to.be.properAddress;
const issuerContract = await ethers.getContractAt('PassCardIssuer', issuer);
const owner = await issuerContract.owner();
expect(owner).to.equal(brandOwner.address);
});
});
describe('AssetFactory', function () {
it('品牌应该能够设置自己的 Issuer', async function () {
await assetFactory.connect(brandOwner).setIssuerForProfile(
profileAddress,
1,
issuerAddress
);
const issuer = await assetFactory.profileIssuers(profileAddress, 1);
expect(issuer).to.equal(issuerAddress);
});
it('平台不应该能够设置品牌的 Issuer', async function () {
await expect(
assetFactory.connect(platformAdmin).setIssuerForProfile(
profileAddress,
1,
issuerAddress
)
).to.be.revertedWith('Only profile owner can set issuer');
});
});
2. 集成测试
javascript
describe('完整流程测试', function () {
it('品牌创建 Profile 应该自动部署 Issuers', async function () {
const tx = await profileFactory.createProfile(profileStruct, brandOwner.address);
const receipt = await tx.wait();
// 验证 Issuers 已部署
const profile = await getProfileFromEvent(receipt);
const assetFactory = await profile.assetFactory();
const passCardIssuer = await assetFactory.profileIssuers(profile, 1);
expect(passCardIssuer).to.be.properAddress;
// 验证 owner
const issuer = await ethers.getContractAt('PassCardIssuer', passCardIssuer);
expect(await issuer.owner()).to.equal(brandOwner.address);
});
it('品牌应该能够使用专属 Issuer 创建资产', async function () {
const assetsInfo = { /* ... */ };
const tx = await profile.connect(brandOwner).issueAsset(assetsInfo);
const receipt = await tx.wait();
// 验证资产已创建
// 验证使用了品牌专属 Issuer
});
});
风险评估
技术风险
| 风险 | 影响 | 概率 | 缓解措施 |
|---|---|---|---|
| Clone Factory 实现错误 | 高 | 低 | 使用 OpenZeppelin Clones 库,经过审计 |
| 权限验证漏洞 | 高 | 低 | 完善的测试覆盖,安全审计 |
| Gas 成本过高 | 中 | 低 | 使用 Clone Factory 已优化 |
| 初始化逻辑错误 | 中 | 中 | 充分的单元测试和集成测试 |
业务风险
| 风险 | 影响 | 概率 | 缓解措施 |
|---|---|---|---|
| 品牌不理解如何配置 | 中 | 中 | 完善的文档和示例 |
| 迁移成本 | 中 | 中 | 提供迁移工具和脚本 |
| 平台失去部分控制权 | 低 | 高 | 这是设计目标,预期结果 |
实施进度
✅ 已完成
-
✅ 核心合约开发
- AssetFactoryV2.sol(包含
getIssuer()和自动回退逻辑) - 所有 Issuer V2 版本(5 个):PassCard, DigitalPoints, Badge, POAP, Ticket
- 所有 Issuer Factory(5 个):使用 EIP-1167 Clone Factory
- IAssetFactoryV2 接口
- AssetFactoryV2.sol(包含
-
✅ 部署脚本
scripts/deploy-v2-platform.js- 平台一键部署脚本
-
✅ 设计文档
- 完整的架构设计
- 详细的技术实现说明
- 使用示例和代码片段
- 权限控制和安全保证说明
⏳ 待实现
-
⏳ S11eProfile 适配 V2
- 方案 A: 创建 S11eProfileV2.sol(推荐,保持向后兼容)
- 方案 B: 修改现有 Profile 支持 V2(需谨慎评估影响)
- 需要添加:
setAssetFactoryV2()方法setIssuer()和setIssuers()便捷方法- 修改
issueAsset()支持传递address(this)给 V2
-
⏳ S11eProfileFactoryV2
- 自动部署品牌专属 Issuers(通过 Issuer Factory)
- 自动在 AssetFactoryV2 中注册
- 可选:支持使用平台默认 Issuers(节省 Gas)
-
⏳ 测试用例
- 单元测试:Issuer Factory、AssetFactoryV2、权限验证
- 集成测试:完整 Profile 创建到资产发行流程
- 安全测试:权限控制、品牌隔离验证
-
⏳ 品牌创建脚本
- 品牌 Profile 创建脚本(带自动部署 Issuers)
- 迁移现有 Profile 到 V2 的脚本
最佳实践
1. Gas 优化策略
策略 A: 使用平台默认 Issuers(推荐新品牌)
- Gas 成本:~0(仅配置)
- 适合:快速开始,后续可替换
策略 B: 部署专属 Issuers(推荐成熟品牌)
- Gas 成本:~225,000 gas
- 适合:需要完全控制和自定义
策略 C: 混合使用
- 部分资产类型用平台默认,部分用自定义
- 灵活平衡成本和自主权
2. 权限管理
Issuer Owner 管理:
- ✅ Issuer owner = 品牌地址(EOA 或多签钱包)
- ✅ 品牌可以授权其他地址使用 Issuer(如果 Issuer 支持)
- ⚠️ 建议使用多签钱包作为 owner(提高安全性)
Profile Owner 管理:
- ✅ Profile owner 可以设置 Issuers
- ✅ Profile owner 可以创建资产
- ⚠️ 确保 Profile owner 地址安全(建议多签)
3. 错误处理
常见错误及处理:
-
AssetFactory: Only profile owner can set issuer- 原因:调用者不是 Profile owner
- 处理:使用 Profile owner 账户调用
-
AssetFactory: Default issuer not set- 原因:平台未设置默认 Issuer
- 处理:平台先设置默认 Issuer,或品牌部署自己的
-
PassCard issuer not set- 原因:品牌未设置 Issuer,且平台默认也未设置
- 处理:品牌先设置 Issuer
4. 升级策略
模板合约升级:
- ✅ 已部署的 Clone 实例不受影响
- ✅ 新部署的实例使用新模板
- ⚠️ 如果使用 UUPS 代理模式,品牌可以选择升级自己的实例
AssetFactoryV2 升级:
- ⚠️ 如果使用代理模式,可以升级
- ⚠️ 如果不使用代理,需要重新部署
常见问题 (FAQ)
Q1: 品牌可以同时使用旧版和 V2 吗?
A: 可以,V2 是独立部署的新合约,不会影响旧版。品牌可以:
- 继续使用旧版 AssetFactory 创建资产
- 切换到 AssetFactoryV2 使用专属 Issuers
- 或同时使用(不同资产类型用不同 Factory)
Q2: 品牌如何知道应该使用哪个 Issuer?
A: 品牌可以:
- 查看
assetFactoryV2.profileIssuers(profileAddress, assetType) - 如果返回
address(0),则使用平台默认 - 查看
assetFactoryV2.defaultIssuers(assetType)获取默认值
Q3: 平台默认 Issuers 可以随时修改吗?
A: 可以,但:
- ✅ 平台可以修改
defaultIssuers - ⚠️ 已经设置的品牌 Issuers 不会受影响(品牌已设置的使用品牌配置)
- ⚠️ 新品牌首次创建资产时,如果未设置 Issuer,会使用最新的默认值
Q4: 品牌可以撤销 Issuer 设置吗?
A: 可以:
- 品牌可以设置
address(0),会使用平台默认 - 品牌可以设置新的 Issuer 地址替换旧的
Q5: Gas 成本会随品牌数量增长吗?
A: 不会:
- 每个品牌的 Issuers 配置存储在
profileIssuers映射中 - 读取 Gas 成本固定(SLOAD)
- 写入 Gas 成本固定(SSTORE)
- 品牌数量不影响单次操作成本
Q6: 当前部署的 S11eProfile 可以直接使用 AssetFactoryV2 吗?
A : 当前不能直接使用,原因:
- 当前 S11eProfile 使用
IAssetFactory接口(旧版) - 旧版
createPassCard()没有profile参数 - V2 版本的
createPassCard()需要profile参数
解决方案:
-
方式 A(推荐):直接调用 AssetFactoryV2(不通过 Profile)
javascript// 直接调用 AssetFactoryV2,手动传递 profile 地址 const passCardAddress = await assetFactoryV2.createPassCard( profileAddress, name, symbol, owner, baseURI, registry, maxSupply ); -
方式 B:等待 S11eProfileV2 或 Profile 适配完成
- 创建新的
S11eProfileV2.sol支持 V2 - 或修改现有 Profile 添加 V2 支持
- 创建新的
Q7: 如何判断是否应该使用 getIssuer() 还是 profileIssuers()?
A:
- 使用
getIssuer():如果你想获取实际使用的 Issuer(包括自动回退到平台默认) - 使用
profileIssuers():如果你想检查品牌是否显式设置了专属 Issuer
示例:
javascript
// 获取实际使用的 Issuer(推荐)
const activeIssuer = await assetFactoryV2.getIssuer(profileAddress, 1);
// 检查是否设置了专属 Issuer
const brandIssuer = await assetFactoryV2.profileIssuers(profileAddress, 1);
if (brandIssuer === ethers.ZeroAddress) {
console.log('使用平台默认 Issuer');
} else {
console.log('使用品牌专属 Issuer');
}
Q8: 创建资产后如何验证使用的是哪个 Issuer?
A: 可以通过以下方式验证:
- 查看事件 :
AssetCreated事件包含资产地址 - 检查 Issuer owner:确认 Issuer 的 owner 是否是品牌地址
- 对比地址:将使用的 Issuer 与品牌专属/平台默认 Issuer 对比
示例:
javascript
// 创建资产后验证
const receipt = await tx.wait();
const event = receipt.logs.find(/* ... */);
const assetAddress = event.args.asset;
// 查看使用的 Issuer
const usedIssuer = await assetFactoryV2.getIssuer(profileAddress, 1);
const issuerContract = await ethers.getContractAt('PassCardIssuerV2', usedIssuer);
const issuerOwner = await issuerContract.owner();
console.log('使用的 Issuer:', usedIssuer);
console.log('Issuer Owner:', issuerOwner);
console.log('是否为品牌专属:', issuerOwner === brandOwner.address);
总结
品牌独立 Issuer 方案实现了:
- ✅ 品牌完全拥有和控制自己的 Issuers
- ✅ 平台无法干涉品牌配置(权限验证保证)
- ✅ 合理的 Gas 成本(~225,000 gas/品牌,使用 Clone Factory)
- ✅ 实现相对简单(V2 版本,不破坏现有合约)
- ✅ 完善的权限控制和安全保证
- ✅ 向后兼容(可以并行使用)
核心优势:
- 去中心化:品牌拥有 Issuer 实例的 owner 权限
- 自主权:品牌可以部署和配置自定义 Issuers
- 隔离性:品牌之间完全隔离,互不影响
- 成本优化:使用 Clone Factory 降低 55% Gas 成本
- 灵活性:品牌可以选择使用平台默认或自定义
这是一个平衡去中心化、成本和实现复杂度的优秀方案,推荐作为 S11e Protocol 的品牌自主控制实施方案。
文档版本 : v2.1
最后更新 : 2025-01-30
维护者: S11e Protocol Team
相关文件:
contracts/core/AssetFactoryV2.sol✅ 已实现contracts/core/issuers/*IssuerV2.sol✅ 已实现contracts/core/issuers/*IssuerFactory.sol✅ 已实现contracts/core/interfaces/IAssetFactoryV2.sol✅ 已实现scripts/deploy-v2-platform.js✅ 部署脚本已创建
待实现:
contracts/core/S11eProfileV2.sol⏳ 或现有 Profile 适配contracts/core/S11eProfileFactoryV2.sol⏳ 自动部署 Issuers