web3品牌RWA资产自主发行设计方案

📋 目录

  1. 概述
  2. 设计目标
  3. 架构设计
  4. 技术实现
  5. 合约设计
  6. 权限控制
  7. 部署流程
  8. 使用示例
  9. 实施步骤
  10. 迁移方案
  11. 测试策略
  12. 风险评估

概述

背景

当前 S11e Protocol 设计中,所有品牌共享同一个 AssetFactory 和统一的 Issuer 合约实例。平台账户拥有 AssetFactory 的 owner 权限,可以随时修改 Issuers 配置,影响所有品牌。这导致:

  • ❌ 品牌缺乏自主权
  • ❌ 平台可以控制所有品牌的资产创建
  • ❌ 品牌无法自定义资产发行逻辑
  • ❌ 品牌之间缺乏隔离

解决方案

品牌独立 Issuer 方案

  • ✅ 平台部署统一的 AssetFactory(支持 Profile 隔离)
  • ✅ 品牌创建 Profile 时,自动为每个品牌部署独立的 Issuer 实例
  • ✅ 将品牌地址设置为每个 Issuer 的 owner
  • ✅ 品牌完全拥有和控制自己的 Issuers
  • ✅ 使用 Clone Factory(EIP-1167)降低 Gas 成本

核心优势

  1. 完全去中心化:品牌拥有自己的 Issuer 实例,owner = 品牌地址
  2. 品牌自主权:品牌可以部署和配置自定义 Issuers
  3. 平台无法控制:权限验证确保只有品牌可以设置自己的 Issuers
  4. Gas 成本合理:~225,000 gas(约 $10-15/品牌,使用 Clone Factory)
  5. 隔离性强:品牌之间完全隔离,互不影响

设计目标

功能目标

  1. 品牌完全拥有 Issuers:每个品牌拥有独立的 Issuer 实例,owner = 品牌地址
  2. 品牌自主配置:品牌可以设置、替换自己的 Issuers
  3. 平台无法控制:平台无法修改品牌的 Issuer 配置
  4. 开箱即用:品牌创建 Profile 后可以立即创建资产
  5. 灵活扩展:品牌可以部署自定义 Issuer 合约

技术目标

  1. Gas 成本优化:使用 Clone Factory 降低部署成本
  2. 向后兼容:可以与现有部署兼容(可选)
  3. 实现简单:基于现有合约,改动最小
  4. 安全性:完善的权限验证机制

架构设计

整体架构

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      平台层(部署一次)                        │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────────┐    ┌──────────────────────┐          │
│  │  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

状态: ✅ 已创建

主要特性

  1. profileIssuers 映射:mapping(address => mapping(uint8 => address)) - 每个品牌独立配置
  2. setIssuerForProfile() 方法:品牌设置自己的 Issuer(权限验证)
  3. getIssuer() 方法:获取 Profile 的 Issuer,自动回退到平台默认(如果未设置)
  4. ✅ 所有 createXxx() 方法:添加 address profile 参数,自动处理 Issuer 查找
    • 优先使用品牌专属 Issuer
    • 如果未设置,自动使用平台默认 Issuer 并写入 profileIssuers(首次使用时)
  5. defaultIssuers 映射:平台默认 Issuers(品牌可选使用)
  6. 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.sol
  • contracts/core/issuers/DigitalPointsIssuerFactory.sol
  • contracts/core/issuers/BadgeIssuerFactory.sol
  • contracts/core/issuers/POAPIssuerFactory.sol
  • contracts/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.sol
  • contracts/core/issuers/DigitalPointsIssuerV2.sol
  • contracts/core/issuers/BadgeIssuerV2.sol
  • contracts/core/issuers/POAPIssuerV2.sol
  • contracts/core/issuers/TicketIssuerV2.sol

主要特性

  1. ✅ 使用 Initializable(OpenZeppelin Upgradeable)
  2. initialize(address _owner) 方法:设置 owner
  3. owner 状态变量和 onlyOwner 修饰符
  4. ✅ 构造函数中禁用初始化(_disableInitializers()) - 模板合约
  5. ✅ 保持原有 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(如果兼容性允许)

主要修改点

  1. 修改 issueAsset() 方法,传递 address(this) 给 AssetFactoryV2
  2. 添加类型检查(判断是 AssetFactory 还是 AssetFactoryV2)
  3. 添加 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(待创建)

功能设计

  1. ✅ 支持自动部署品牌专属 Issuers(通过 Issuer Factory)
  2. ✅ 自动在 AssetFactoryV2 中注册 Issuers
  3. ✅ 自动设置 Profile 的 AssetFactoryV2
  4. ✅ 可选:支持使用平台默认 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) 品牌便捷方法

安全保证

  1. 平台无法控制品牌配置

    solidity 复制代码
    // AssetFactory.setIssuerForProfile()
    require(
        profileContract.hasRole(DEFAULT_ADMIN_ROLE, msg.sender),
        "Only profile owner can set issuer"
    );
    • ✅ 平台账户不是 Profile 的 owner,无法调用
  2. 品牌只能设置自己的配置

    • profileIssuers[profile][assetType] 映射确保品牌之间隔离
    • ✅ 品牌只能修改自己的映射,无法影响其他品牌
  3. 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: 合约开发

状态: ✅ 已完成核心合约

  1. AssetFactoryV2.sol - 已创建

    • profileIssuers 映射
    • setIssuerForProfile() 方法(权限验证)
    • ✅ 所有 createXxx() 方法支持 Profile 参数
    • defaultIssuers 映射和 setDefaultIssuer() 方法
  2. Issuer V2 合约 - 已创建

    • PassCardIssuerV2.sol
    • DigitalPointsIssuerV2.sol
    • BadgeIssuerV2.sol
    • POAPIssuerV2.sol
    • TicketIssuerV2.sol
    • ✅ 全部支持 initialize()owner 权限
  3. Issuer Factory 合约 - 已创建

    • PassCardIssuerFactory.sol
    • DigitalPointsIssuerFactory.sol
    • BadgeIssuerFactory.sol
    • POAPIssuerFactory.sol
    • TicketIssuerFactory.sol
    • ✅ 全部使用 EIP-1167 Clone Factory
  4. S11eProfile 适配 - 待实现

    • 方案 A: 创建 S11eProfileV2.sol(推荐)
    • 方案 B: 在现有 Profile 中添加 V2 支持
    • 需要修改 issueAsset() 传递 address(this)
    • 需要添加 setIssuer()setIssuers() 方法
  5. S11eProfileFactoryV2 - 待创建

    • 自动部署品牌专属 Issuers
    • 自动在 AssetFactoryV2 中注册
    • 可选:支持使用平台默认(节省 Gas)

阶段 2: 测试(1 周)

  1. ✅ 单元测试

    • Issuer Factory 部署测试
    • AssetFactory 权限验证测试
    • Profile 创建流程测试
  2. ✅ 集成测试

    • 完整 Profile 创建流程
    • 品牌替换 Issuer 测试
    • 品牌创建资产测试
  3. ✅ 安全测试

    • 权限控制验证
    • 平台无法控制品牌配置
    • 品牌之间隔离验证

阶段 3: 部署(1 周)

  1. ✅ 部署到测试网

    • Sepolia 测试网部署
    • 完整功能验证
  2. ✅ 主网部署

    • 审计(如果需要)
    • 主网部署
    • 配置和验证

阶段 4: 迁移(可选)

  1. ✅ 现有 Profile 迁移
    • 为现有 Profile 部署 Issuers
    • 更新 AssetFactory 配置

迁移方案

场景 1: 新部署 V2 版本(推荐)

适用于:全新部署或愿意迁移的场景

优势

  • ✅ 不影响现有部署
  • ✅ 可以并行运行(旧版和 V2)
  • ✅ 逐步迁移策略

步骤

  1. ✅ 部署 AssetFactoryV2(已完成)
  2. ✅ 部署所有 Issuer V2 模板和 Factory(已完成)
  3. ⏳ 创建 S11eProfileFactoryV2(待实现)
  4. 现有 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)
  • 现有品牌继续使用旧版,可选择性迁移

实现

  1. 部署 V2 版本(并行运行)
  2. ProfileFactory 检查版本标志
  3. 新 Profile 自动使用 V2
  4. 现有 Profile 保持不变或手动迁移

场景 3: 完全迁移

适用于:所有品牌都需要迁移的场景

步骤

  1. 为所有现有 Profile 部署 V2 Issuers
  2. 在 AssetFactoryV2 中注册所有配置
  3. 更新所有 Profile 使用 AssetFactoryV2
  4. 停用旧版 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 已优化
初始化逻辑错误 充分的单元测试和集成测试

业务风险

风险 影响 概率 缓解措施
品牌不理解如何配置 完善的文档和示例
迁移成本 提供迁移工具和脚本
平台失去部分控制权 这是设计目标,预期结果

实施进度

✅ 已完成

  1. 核心合约开发

    • AssetFactoryV2.sol(包含 getIssuer() 和自动回退逻辑)
    • 所有 Issuer V2 版本(5 个):PassCard, DigitalPoints, Badge, POAP, Ticket
    • 所有 Issuer Factory(5 个):使用 EIP-1167 Clone Factory
    • IAssetFactoryV2 接口
  2. 部署脚本

    • scripts/deploy-v2-platform.js - 平台一键部署脚本
  3. 设计文档

    • 完整的架构设计
    • 详细的技术实现说明
    • 使用示例和代码片段
    • 权限控制和安全保证说明

⏳ 待实现

  1. S11eProfile 适配 V2

    • 方案 A: 创建 S11eProfileV2.sol(推荐,保持向后兼容)
    • 方案 B: 修改现有 Profile 支持 V2(需谨慎评估影响)
    • 需要添加:
      • setAssetFactoryV2() 方法
      • setIssuer()setIssuers() 便捷方法
      • 修改 issueAsset() 支持传递 address(this) 给 V2
  2. S11eProfileFactoryV2

    • 自动部署品牌专属 Issuers(通过 Issuer Factory)
    • 自动在 AssetFactoryV2 中注册
    • 可选:支持使用平台默认 Issuers(节省 Gas)
  3. 测试用例

    • 单元测试:Issuer Factory、AssetFactoryV2、权限验证
    • 集成测试:完整 Profile 创建到资产发行流程
    • 安全测试:权限控制、品牌隔离验证
  4. 品牌创建脚本

    • 品牌 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. 错误处理

常见错误及处理

  1. AssetFactory: Only profile owner can set issuer

    • 原因:调用者不是 Profile owner
    • 处理:使用 Profile owner 账户调用
  2. AssetFactory: Default issuer not set

    • 原因:平台未设置默认 Issuer
    • 处理:平台先设置默认 Issuer,或品牌部署自己的
  3. 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 参数

解决方案

  1. 方式 A(推荐):直接调用 AssetFactoryV2(不通过 Profile)

    javascript 复制代码
    // 直接调用 AssetFactoryV2,手动传递 profile 地址
    const passCardAddress = await assetFactoryV2.createPassCard(
      profileAddress, name, symbol, owner, baseURI, registry, maxSupply
    );
  2. 方式 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: 可以通过以下方式验证:

  1. 查看事件AssetCreated 事件包含资产地址
  2. 检查 Issuer owner:确认 Issuer 的 owner 是否是品牌地址
  3. 对比地址:将使用的 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 版本,不破坏现有合约)
  • ✅ 完善的权限控制和安全保证
  • ✅ 向后兼容(可以并行使用)

核心优势

  1. 去中心化:品牌拥有 Issuer 实例的 owner 权限
  2. 自主权:品牌可以部署和配置自定义 Issuers
  3. 隔离性:品牌之间完全隔离,互不影响
  4. 成本优化:使用 Clone Factory 降低 55% Gas 成本
  5. 灵活性:品牌可以选择使用平台默认或自定义

这是一个平衡去中心化、成本和实现复杂度的优秀方案,推荐作为 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
相关推荐
元宇宙时间3 小时前
Nine.fun:连接现实娱乐与Web3经济的全新生态
人工智能·金融·web3·区块链
只会写Bug的程序员3 小时前
【职业方向】2026小目标,从web开发转型web3开发【一】
前端·web3
MicroTech20255 小时前
微算法科技(NASDAQ MLGO):以隐私计算区块链筑牢多方安全计算(MPC)安全防线
科技·安全·区块链
野老杂谈6 小时前
【Solidity 从入门到精通】第3章 Solidity 基础语法详解
web3·solidity
leijiwen6 小时前
S11e Protocol 数字身份体系架构白皮书
架构·web3·区块链·品牌·rwa
野老杂谈6 小时前
【Solidity 从入门到精通】第2章 Solidity 语言概览与环境搭建
web3·区块链·智能合约·solidity·remix ide
MicroTech20251 天前
微算法科技(NASDAQ MLGO):DPoS驱动区块链治理与DAO机制融合,共筑Web3.0坚实基石
科技·web3·区块链
野老杂谈1 天前
【Solidity 从入门到精通】前言
web3·智能合约·solidity·以太坊·dapp