前言
本文围绕 ERC7281 标准展开全面梳理,涵盖其核心定义、核心功能、行业痛点解决方案及典型应用场景,并结合 Hardhat V3 开发环境与 OpenZeppelin 工具库,完整实现了 ERC7281 智能租赁合约的开发、部署与测试全流程落地,形成 “标准理论 + 实战落地” 的完整内容体系。
概述
ERC7281(又称 xERC-20)是以太坊改进提案(EIP),核心是将跨链代币控制权从桥接方交还给发行方,通过扩展 ERC-20 接口实现多桥兼容、动态限额与统一资产表示,解决传统跨链的流动性碎片化、安全风险集中与资产不可互换问题,适用于 DeFi、稳定币、RWA 等多链场景。
是什么:核心定义与架构
-
定位:ERC-20 的扩展标准,保留基础接口,新增跨链治理与多桥协作能力。
-
核心角色:
- 发行方:掌控跨链权限,可授权 / 撤销桥接方、设置单桥铸造限额。
- 桥接方:经授权后在目标链铸造 / 销毁代币,受额度与权限约束。
- Lockbox(可选) :兼容层合约,将传统 ERC-20 封装为 xERC-20,支持存量资产升级。
-
关键接口:
接口 功能 authorizeBridge授权桥接方在指定链操作 setBridgeLimit为桥接方设置铸造 / 销毁限额 mintRemote/burnRemote跨链铸造 / 销毁标准代币 isBridgeAuthorized查询桥接方权限状态
能做什么:核心功能与价值
-
发行方主权管理
- 自主选择桥接方并动态调整限额,降低单点风险。
- 桥接方被攻击时,损失上限为该桥额度,且可快速下架问题桥接方。
-
统一跨链资产表示
- 不同桥接方铸造的同币种资产完全可互换(1:1),消除 slippage 与流动性分裂。
- 新链 /rollup 无需重复部署流动性,资产跨域流动更高效。
-
兼容与可扩展性
- 向下兼容 ERC-20,支持通过 Lockbox 平滑迁移存量资产。
- 多桥并行协作,激励桥接方提升安全性以争取更高额度。
解决了什么问题:行业痛点对比
| 传统跨链痛点 | ERC7281 解决方案 | |
|---|---|---|
| 控制权旁落 | 桥接方掌控跨链资产,发行方无法干预 | 发行方持有权限,可实时调整桥接策略 |
| 流动性分裂 | 不同桥铸造的同币种资产不可互换,形成流动性孤岛 | 统一资产表示,多桥铸造的代币完全 fungible |
| 安全风险集中 | 单一桥被攻击可能导致资产全量损失 | 单桥限额隔离风险,快速下架问题桥接方 |
| 用户体验差 | 跨链存在 slippage,资产迁移流程复杂 | 无滑点跨链,发行方治理升级不影响用户持仓 |
| 扩展成本高 | 新链需重复部署流动性,资产覆盖效率低 | 统一资产模型降低新链适配成本 |
行业应用:典型场景
-
稳定币跨链部署
- USDC/USDT 等可通过 ERC7281 在多链发行统一版本,避免多桥并行导致的流动性分散,提升资金利用率与用户体验。
-
DeFi 跨链协议
- Aave、Uniswap 等可基于 xERC-20 实现跨链资产无缝接入,降低多链部署复杂度,增强协议间 composability。
-
RWA(现实世界资产)上链
- 国债、房产等资产代币化后,通过 ERC7281 跨链流通,保持资产一致性与合规可控性。
-
NFT 跨链(扩展方向)
- 社区已提出 ERC7281 扩展方案(如 Sovereign Bridged NFTs),复用授权与限额机制,解决 NFT 跨链碎片化问题
智能合约开发、部署、测试
智能合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";
/**
* @title ERC7281NFT
* @dev 基于ERC-7281标准的NFT租赁合约,兼容OpenZeppelin 5.4.0
* 核心功能:租赁收益分成、动态权限、紧急回收、碎片化租赁
*/
contract ERC7281NFT is ERC721, Ownable, ReentrancyGuard, Pausable {
// ========== 核心数据结构 ==========
// NFT租赁信息
struct LeaseInfo {
address user; // 当前使用者
uint64 expires; // 租赁到期时间戳(Unix秒)
uint256 rentAmount; // 总租金(Wei)
uint256 platformFeeRate; // 平台费率(万分比,如500=5%)
uint256 paidAmount; // 已支付租金
bool isEmergencyTerminated; // 是否紧急终止
uint256 penaltyAmount; // 紧急终止违约金
bytes32[] allowedPermissions; // 使用者允许的权限(如"GAME_USE", "METADATA_VIEW")
mapping(address => uint256) fragmentShares; // 碎片化租赁份额(仅当拆分时生效)
}
// ========== 状态变量 ==========
// 平台收款地址
address public platformWallet;
// 每个NFT的租赁信息
mapping(uint256 => LeaseInfo) private _leaseInfos;
// 权限注册表(全局)
mapping(bytes32 => bool) public supportedPermissions;
// ========== 事件定义 ==========
// 设置租赁条款
event LeaseTermsSet(
uint256 indexed tokenId,
address indexed user,
uint64 expires,
uint256 rentAmount,
uint256 platformFeeRate,
bytes32[] allowedPermissions
);
// 租金支付
event RentPaid(uint256 indexed tokenId, address indexed payer, uint256 amount);
// 收益提取
event RevenueClaimed(uint256 indexed tokenId, address indexed recipient, uint256 amount);
// 紧急终止租赁
event LeaseEmergencyTerminated(uint256 indexed tokenId, address indexed owner, uint256 penaltyAmount);
// 碎片化租赁设置
event FragmentLeaseSet(uint256 indexed tokenId, address indexed user, uint256 share);
// ========== 错误定义 ==========
error TokenNotExists(uint256 tokenId);
error NotTokenOwner(uint256 tokenId, address caller);
error LeaseExpired(uint256 tokenId);
error InsufficientPayment(uint256 required, uint256 paid);
error PermissionNotSupported(bytes32 permission);
error InvalidShareAmount(uint256 totalShare);
error NoRevenueToClaim();
// ========== 构造函数 ==========
constructor(
string memory name,
string memory symbol,
address initialPlatformWallet,
uint256 initialPlatformFeeRate
) ERC721(name, symbol) Ownable(msg.sender) {
require(initialPlatformWallet != address(0), "Platform wallet cannot be zero");
require(initialPlatformFeeRate <= 10000, "Fee rate cannot exceed 100%");
platformWallet = initialPlatformWallet;
// 初始化默认支持的权限
supportedPermissions["GAME_USE"] = true;
supportedPermissions["METADATA_VIEW"] = true;
supportedPermissions["TRANSFER_USE"] = false; // 禁止使用者转让NFT
}
// ========== 核心功能:设置租赁条款(ERC-7281核心) ==========
/**
* @dev 设置NFT租赁条款,仅NFT所有者可调用
* @param tokenId NFT ID
* @param user 租赁使用者地址
* @param expires 租赁到期时间戳(Unix秒)
* @param rentAmount 总租金(Wei)
* @param platformFeeRate 平台费率(万分比,如500=5%)
* @param allowedPermissions 允许使用者的权限列表
*/
function setLeaseTerms(
uint256 tokenId,
address user,
uint64 expires,
uint256 rentAmount,
uint256 platformFeeRate,
bytes32[] calldata allowedPermissions
) external nonReentrant whenNotPaused {
// 修复点1:用_ownerOf替代_exists判断NFT是否存在(5.x版本兼容)
try this.ownerOf(tokenId) {
// NFT存在,继续执行
} catch {
revert TokenNotExists(tokenId);
}
if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(tokenId, msg.sender);
if (platformFeeRate > 10000) revert InsufficientPayment(10000, platformFeeRate);
// 校验权限是否都被支持
for (uint256 i = 0; i < allowedPermissions.length; i++) {
if (!supportedPermissions[allowedPermissions[i]]) {
revert PermissionNotSupported(allowedPermissions[i]);
}
}
LeaseInfo storage lease = _leaseInfos[tokenId];
lease.user = user;
lease.expires = expires;
lease.rentAmount = rentAmount;
lease.platformFeeRate = platformFeeRate;
lease.isEmergencyTerminated = false;
lease.penaltyAmount = rentAmount / 10; // 违约金默认10%租金
lease.allowedPermissions = allowedPermissions;
emit LeaseTermsSet(tokenId, user, expires, rentAmount, platformFeeRate, allowedPermissions);
}
// ========== 核心功能:支付租金 ==========
function payRent(uint256 tokenId) external payable nonReentrant whenNotPaused {
// 修复点2:用_ownerOf替代_exists判断NFT是否存在
try this.ownerOf(tokenId) {
// NFT存在,继续执行
} catch {
revert TokenNotExists(tokenId);
}
LeaseInfo storage lease = _leaseInfos[tokenId];
// 校验租赁未到期且未紧急终止
if (block.timestamp > lease.expires && lease.expires != 0) revert LeaseExpired(tokenId);
if (lease.isEmergencyTerminated) revert LeaseExpired(tokenId);
// 校验支付金额足够
uint256 remaining = lease.rentAmount - lease.paidAmount;
if (msg.value < remaining) revert InsufficientPayment(remaining, msg.value);
lease.paidAmount += msg.value;
emit RentPaid(tokenId, msg.sender, msg.value);
}
// ========== 核心功能:提取租赁收益(ERC-7281核心) ==========
function claimRevenue(uint256 tokenId) external nonReentrant whenNotPaused returns (uint256) {
// 修复点3:用_ownerOf替代_exists判断NFT是否存在
try this.ownerOf(tokenId) {
// NFT存在,继续执行
} catch {
revert TokenNotExists(tokenId);
}
LeaseInfo storage lease = _leaseInfos[tokenId];
// 仅当租金已全额支付且租赁到期/终止时可提取
bool canClaim = (lease.paidAmount >= lease.rentAmount) &&
(block.timestamp > lease.expires || lease.isEmergencyTerminated);
if (!canClaim) revert NoRevenueToClaim();
// 计算收益分配:所有者 + 平台
uint256 platformFee = (lease.rentAmount * lease.platformFeeRate) / 10000;
uint256 ownerRevenue = lease.rentAmount - platformFee;
// 转账给平台和NFT所有者
if (platformFee > 0) {
payable(platformWallet).transfer(platformFee);
}
payable(ownerOf(tokenId)).transfer(ownerRevenue);
emit RevenueClaimed(tokenId, ownerOf(tokenId), ownerRevenue);
emit RevenueClaimed(tokenId, platformWallet, platformFee);
return ownerRevenue;
}
// ========== 核心功能:紧急终止租赁 ==========
function emergencyTerminateLease(uint256 tokenId) external nonReentrant whenNotPaused {
// 修复点4:用_ownerOf替代_exists判断NFT是否存在
try this.ownerOf(tokenId) {
// NFT存在,继续执行
} catch {
revert TokenNotExists(tokenId);
}
if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(tokenId, msg.sender);
LeaseInfo storage lease = _leaseInfos[tokenId];
lease.isEmergencyTerminated = true;
// 向使用者支付违约金(从所有者余额扣除)
if (lease.penaltyAmount > 0 && lease.user != address(0)) {
payable(lease.user).transfer(lease.penaltyAmount);
}
emit LeaseEmergencyTerminated(tokenId, msg.sender, lease.penaltyAmount);
}
// ========== 核心功能:设置碎片化租赁份额 ==========
function setFragmentLease(
uint256 tokenId,
address user,
uint256 share
) external nonReentrant whenNotPaused {
// 修复点5:用_ownerOf替代_exists判断NFT是否存在
try this.ownerOf(tokenId) {
// NFT存在,继续执行
} catch {
revert TokenNotExists(tokenId);
}
if (ownerOf(tokenId) != msg.sender) revert NotTokenOwner(tokenId, msg.sender);
LeaseInfo storage lease = _leaseInfos[tokenId];
lease.fragmentShares[user] = share;
// 校验总份额不超过100%
uint256 totalShare;
for (uint256 i = 0; i < lease.allowedPermissions.length; i++) {
// 简化校验:实际场景需遍历所有fragmentShares
totalShare += share;
}
if (totalShare > 10000) revert InvalidShareAmount(totalShare);
emit FragmentLeaseSet(tokenId, user, share);
}
// ========== 视图函数:查询租赁信息 ==========
// 查询当前使用者
function userOf(uint256 tokenId) external view returns (address) {
LeaseInfo storage lease = _leaseInfos[tokenId];
if (block.timestamp <= lease.expires && !lease.isEmergencyTerminated) {
return lease.user;
}
return address(0);
}
// 查询租赁到期时间
function userExpires(uint256 tokenId) external view returns (uint64) {
return _leaseInfos[tokenId].expires;
}
// 查询使用者权限
function getUserPermissions(uint256 tokenId) external view returns (bytes32[] memory) {
return _leaseInfos[tokenId].allowedPermissions;
}
// 查询碎片化租赁份额
function getFragmentShare(uint256 tokenId, address user) external view returns (uint256) {
return _leaseInfos[tokenId].fragmentShares[user];
}
// ========== 管理员功能 ==========
// 新增支持的权限
function addSupportedPermission(bytes32 permission) external onlyOwner {
supportedPermissions[permission] = true;
}
// 修改平台钱包地址
function setPlatformWallet(address newWallet) external onlyOwner {
require(newWallet != address(0), "New wallet cannot be zero");
platformWallet = newWallet;
}
// 暂停/恢复合约
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
// ========== 辅助函数 ==========
// 铸造NFT(仅示例)
function mint(address to, uint256 tokenId) external onlyOwner {
_mint(to, tokenId);
}
// ========== 修复点6:移除重写的_transfer函数 ==========
// 说明:OpenZeppelin 5.x中_transfer为非虚函数,不可重写;
// 若需自定义转让逻辑,可重写safeTransferFrom或使用_beforeTokenTransfer钩子
}
合约编译指令:npx hardhat compile
智能合约部署
// scripts/deploy.js
import { network, artifacts } from "hardhat";
async function main() {
// 连接网络
const { viem } = await network.connect({ network: network.name });//指定网络进行链接
// 获取客户端
const [deployer] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
const deployerAddress = deployer.account.address;
console.log("部署者的地址:", deployerAddress);
// 加载合约
const artifact = await artifacts.readArtifact("ERC7281NFT");
const hash = await deployer.deployContract({
abi: artifact.abi,//获取abi
bytecode: artifact.bytecode,//硬编码
args: ["MyERC7281NFT","MINFT",deployerAddress,500n],//nft名称,nft符号,部署者地址,price
});
// 等待确认并打印地址
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log("合约地址:", receipt.contractAddress);
}
main().catch(console.error);
合约部署指令:npx hardhat run ./scripts/xxx.ts
智能合约测试
import assert from "node:assert/strict";
import { describe, it,beforeEach } from "node:test";
import { formatEther,parseEther,keccak256,toHex,hexToBytes, bytesToHex,zeroHash,encodePacked ,toBytes,pad,hexToBigInt } from 'viem'
import { network } from "hardhat";
describe("ERC7281智能合约测试", async function () {
let viem: any;
let publicClient: any;
let owner: any, user1: any, user2: any, user3: any;
let deployerAddress: string;
let MyERC7281NFT: any;
// 测试常量
const RENT_AMOUNT = parseEther("1"); // 1 ETH 租金
const PLATFORM_FEE_RATE = 500n; // 5% 平台费率(万分比)
const LEASE_DURATION = 3600n; // 1小时租赁期
const TOKEN_ID = 1;
const TOKEN_URI = "https://zygomorphic-magenta-bobolink.myfilebase.com/ipfs/QmQT8VpmWQVhUhoDCEK1mdHXaFaJ3KawkRxHm96GUhrXLB";
const encodeBytes32String=(str) => {
// 1. 将字符串转为UTF-8字节数组
const bytes = toBytes(str, { size: 32 });
// 2. 补零到32字节(不足时右侧补0,超过时截断)
const paddedBytes = pad(bytes, { size: 32, dir: "right" });
// 3. 转为十六进制字符串(带0x前缀)
return `0x${Buffer.from(paddedBytes).toString("hex")}`;
}
beforeEach (async function () {
const { viem } = await network.connect();
publicClient = await viem.getPublicClient();//创建一个公共客户端实例用于读取链上数据(无需私钥签名)。
[owner,user1,user2,user3] = await viem.getWalletClients();//获取第一个钱包客户端 写入联合交易
deployerAddress = owner.account.address;//钱包地址
MyERC7281NFT = await viem.deployContract("ERC7281NFT", [
"My Royalty NFT",
"MRNFT",
deployerAddress,
PLATFORM_FEE_RATE,
]);//部署合约
console.log("MyERC7281NFT合约地址:", MyERC7281NFT.address);
});
it("应该创建一个NFT", async function () {
//查询nft名称和符号
const name= await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "name",
args: [],
});
const symbol= await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "symbol",
args: [],
});
console.log("nftINFO:", name,symbol);
//铸造nft
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "mint",
args: [user1.account.address,TOKEN_ID],
});
//查询nft所有者
const ownerOf = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "ownerOf",
args: [TOKEN_ID],
});
console.log("nft所有者:", ownerOf);
});
it("应该成功设置NFT租赁条款", async function () {
//铸造nft
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "mint",
args: [owner.account.address,TOKEN_ID],
});
const ownerOf = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "ownerOf",
args: [TOKEN_ID],
});
console.log("nft所有者:", ownerOf);
// 计算租赁到期时间(当前时间 + 1小时)
const blockNumber = await publicClient.getBlockNumber();
const block = await publicClient.getBlock({ blockNumber });
const expires = BigInt(block.timestamp) + LEASE_DURATION;
console.log(expires)
console.log(encodeBytes32String("GAME_USE"));
// 允许的权限:GAME_USE
const allowedPermissions = [encodeBytes32String("GAME_USE")];
// 所有者设置租赁条款
const hash= await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "setLeaseTerms",
args: [TOKEN_ID,user1.account.address,expires,RENT_AMOUNT,PLATFORM_FEE_RATE,allowedPermissions],
});
await publicClient.waitForTransactionReceipt({ hash });
console.log("租赁条款设置成功,交易哈希:", hash);
await publicClient.waitForTransactionReceipt({ hash });
// 验证租赁信息
const user = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "userOf",
args: [TOKEN_ID],
});
const expiration = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "userExpires",
args: [TOKEN_ID],
});
const permissions = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "getUserPermissions",
args: [TOKEN_ID],
});
console.log("user:", user);
console.log("expiration:", expiration);
console.log("permissions:", permissions);
});
it("应该成功支付租金", async function () {
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "mint",
args: [owner.account.address,TOKEN_ID],
});
const ownerOf = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "ownerOf",
args: [TOKEN_ID],
});
console.log("nft所有者:", ownerOf);
// 记录支付前的余额
const userBalanceBefore = await publicClient.getBalance({
address: user1.account.address
});
console.log(formatEther(userBalanceBefore))
// 使用者支付租金(1 ETH)
const hash = await user1.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "payRent",
args: [TOKEN_ID],
});
console.log("支付租金交易哈希:", hash);
await publicClient.waitForTransactionReceipt({ hash });
// 记录支付后的余额
const userBalanceAfter = await publicClient.getBalance({
address: user1.account.address
});
console.log(formatEther(userBalanceAfter))
console.log(formatEther(userBalanceBefore - RENT_AMOUNT))
});
// 测试4:设置碎片化租赁份额
it("应该成功设置碎片化租赁份额", async function () {
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "mint",
args: [owner.account.address,TOKEN_ID],
});
const ownerOf = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "ownerOf",
args: [TOKEN_ID],
});
console.log("nft所有者:", ownerOf);
const share = 5000n; // 50% 份额
// 所有者设置碎片化租赁份额
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "setFragmentLease",
args: [TOKEN_ID,user1.account.address,share],
});
// 验证份额
const fragmentShare = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "getFragmentShare",
args: [TOKEN_ID,user1.account.address],
});
console.log("用户",user1.account.address,"的碎片化租赁份额:", fragmentShare);
});
it("应该成功紧急终止租赁", async function () {
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "mint",
args: [owner.account.address,TOKEN_ID],
});
const ownerOf = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "ownerOf",
args: [TOKEN_ID],
});
console.log("nft所有者:", ownerOf);
// 所有者紧急终止租赁
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "emergencyTerminateLease",
args: [TOKEN_ID],
});
// 验证租赁已终止(使用者变为0地址)
const user = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "userOf",
args: [TOKEN_ID],
});
console.log("用户",user1.account.address,"的租赁已终止,使用者:", user);
});
// 测试6:提取租赁收益
it("应该成功提取租赁收益", async function () {
// ========== 步骤1:铸造NFT给user1(所有者) ==========
await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "mint",
args: [user1.account.address, TOKEN_ID],
});
// 验证NFT所有者
const nftOwner = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "ownerOf",
args: [TOKEN_ID],
});
console.log("NFT所有者地址:", nftOwner);
console.log("预期所有者地址:", user1.account.address);
// expect(nftOwner.toLowerCase()).to.equal(user1.account.address.toLowerCase());
console.log("用户", user1.account.address, "的租赁已终止,使用者:", nftOwner);
// ========== 步骤2:设置租赁条款(关键:用user1账户调用) ==========
const blockNumber = await publicClient.getBlockNumber();
const block = await publicClient.getBlock({ blockNumber });
const expires = BigInt(block.timestamp) + LEASE_DURATION; // 未来到期时间(允许支付租金)
console.log("租赁到期时间:", expires);
// 核心修复:用user1(NFT所有者)调用setLeaseTerms,而非owner
const setLeaseTermshash = await user1.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "setLeaseTerms",
args: [
TOKEN_ID,
user2.account.address, // 租户地址
expires,
RENT_AMOUNT, // 1 ETH
PLATFORM_FEE_RATE, // 500 = 5%
[encodeBytes32String("GAME_USE")] // 正确的权限值
],
account: user1.account, // 显式指定账户,确保使用user1
});
await publicClient.waitForTransactionReceipt({ hash: setLeaseTermshash }); // 修复参数名错误
console.log("租赁条款设置成功,交易哈希:", setLeaseTermshash);
// ========== 步骤3:租户支付租金(租赁未过期) ==========
const payTxHash = await user2.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "payRent",
args: [TOKEN_ID],
value: RENT_AMOUNT, // 支付1 ETH租金
account: user2.account,
});
await publicClient.waitForTransactionReceipt({ hash: payTxHash });
console.log("租金支付完成,交易哈希:", payTxHash);
// ========== 步骤4:更新租赁到期时间为过去(满足提取条件) ==========
const expiredTime = BigInt(Math.floor(Date.now() / 1000) - 3600); // 1小时前过期
const updateLeaseHash = await user1.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "setLeaseTerms",
args: [
TOKEN_ID,
user2.account.address,
expiredTime, // 更新为已过期
RENT_AMOUNT,
PLATFORM_FEE_RATE,
[encodeBytes32String("GAME_USE")]
],
account: user1.account,
});
await publicClient.waitForTransactionReceipt({ hash: updateLeaseHash });
console.log("租赁到期时间更新为已过期,交易哈希:", updateLeaseHash);
// ========== 步骤5:记录提取前的余额 ==========
const ownerBalanceBefore = await publicClient.getBalance({
address: user1.account.address
});
const platformBalanceBefore = await publicClient.getBalance({
address: owner.account.address
});
const contractBalanceBefore = await publicClient.getBalance({
address: MyERC7281NFT.address
});
console.log("\n=== 提取收益前余额 ===");
console.log("合约余额:", formatEther(contractBalanceBefore));
console.log("所有者余额:", formatEther(ownerBalanceBefore));
console.log("平台余额:", formatEther(platformBalanceBefore));
// ========== 步骤6:提取收益(用user1账户) ==========
const claimTxHash = await user1.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "claimRevenue",
args: [TOKEN_ID],
account: user1.account, // 确保使用NFT所有者账户
});
await publicClient.waitForTransactionReceipt({ hash: claimTxHash });
console.log("收益提取完成,交易哈希:", claimTxHash);
// ========== 步骤7:验证收益 ==========
const platformFee = (RENT_AMOUNT * PLATFORM_FEE_RATE) / 10000n; // 5%手续费
const ownerRevenue = RENT_AMOUNT - platformFee; // 95%归所有者
console.log("预期平台收益:", formatEther(platformFee));
console.log("预期所有者收益:", formatEther(ownerRevenue));
// 提取后余额
const ownerBalanceAfter = await publicClient.getBalance({
address: user1.account.address
});
const platformBalanceAfter = await publicClient.getBalance({
address: owner.account.address
});
const contractBalanceAfter = await publicClient.getBalance({
address: MyERC7281NFT.address
});
console.log("\n=== 提取收益后余额 ===");
console.log("所有者收益:", formatEther(ownerBalanceAfter));
console.log("平台收益:", formatEther(platformBalanceAfter));
console.log("合约余额:", formatEther(contractBalanceAfter));
});
it("应该成功修改平台钱包地址", async function () {
const newPlatformWallet = "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc";
// 部署者(合约所有者)修改平台钱包
const hash = await owner.writeContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "setPlatformWallet",
args: [newPlatformWallet],
});
// 验证平台钱包已更新
await publicClient.waitForTransactionReceipt({ hash });
const platformWallet = await publicClient.readContract({
address: MyERC7281NFT.address,
abi: MyERC7281NFT.abi,
functionName: "platformWallet",
args: [],
});
console.log("平台钱包:", platformWallet);
console.log("new钱包:", newPlatformWallet);
});
});
合约测试指令:npx hardhat test ./test/xxx.ts
总结
至此,ERC7281 智能租赁合约从理论到实践的全流程落地。理论上,系统梳理了 ERC7281 标准的核心定义、功能价值与行业应用,明确其解决传统跨链痛点的核心优势;实践中,基于 Hardhat V3 与 OpenZeppelin,完成合约的开发、部署与全链路测试,形成可复用的技术方案。