链上房产 RWA 技术架构全解析:基于 ERC‑1155 的资产代币化、碎片化交易与自动分红实战

  • 木西
  • 发布于 13小时前
  • 阅读 46

前言随着区块链技术与真实世界资产(RWA,RealWorldAssets)融合趋势的加速,房产代币化凭借其庞大的市场规模和明确的收益模式,成为最具落地潜力的RWA赛道之一。传统房产投资存在门槛高、流动性差、分红不透明等痛点,而区块链技术可通过资产代币化、去中心化交易和自动化分红,重构房

前言

随着区块链技术与真实世界资产(RWA, Real World Assets)融合趋势的加速,房产代币化凭借其庞大的市场规模和明确的收益模式,成为最具落地潜力的 RWA 赛道之一。传统房产投资存在门槛高、流动性差、分红不透明等痛点,而区块链技术可通过资产代币化、去中心化交易和自动化分红,重构房产投资的底层逻辑。

本文将深度拆解一套经实战验证的链上房产 RWA 技术架构,完整覆盖资产代币化二级市场交易租金自动分红三大核心模块,提供基于 Solidity 0.8.24 的合约实现、Hardhat+Viem 的测试方案及自动化部署脚本,为开发者落地房产 RWA 项目提供可直接复用的技术范式。

一、系统架构设计

1.1 代币标准选型:为何选择 ERC-1155?

房产 RWA 场景需要同时支持 "单套房产碎片化持有" 和 "多套房产统一管理",ERC-20(同质化代币)和 ERC-721(非同质化代币)均无法高效满足需求,而 ERC-1155 的多资产特性成为最优解。

特性 ERC-20 ERC-721 ERC-1155
多房产支持 需部署多合约,管理成本高 需部署多合约,Gas 成本高 单合约多 ID,统一管理
碎片化持有 支持,但无法区分资产 ID 不支持,仅代表完整所有权 支持,精准对应单套房产
Gas 效率 高,但批量操作无优化 低,单资产单交易 批量操作优化,成本降低 30%+
元数据灵活性 低,仅支持全局元数据 高,单资产独立元数据 高,兼容多资产元数据规范

核心设计逻辑:每个tokenId对应一套独立房产,该 ID 下的代币数量代表房产所有权份额(如 tokenId=1 发行 1000 枚,每枚代表 0.1% 的房产所有权),用户持有份额与房产收益分红直接挂钩。

1.2 系统组件全景

┌─────────────────────────────────────────────────────────┐
│                    RWAAsset (ERC-1155)                   │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐   │
│  │  房产A (ID=1) │  │  房产B (ID=2) │  │  房产C (ID=3) │   │
│  │   份额: 1000  │  │   份额: 2000  │  │   份额: 1500  │   │
│  └──────────────┘  └──────────────┘  └──────────────┘   │
└─────────────────────────────────────────────────────────┘
           │                    │                    │
           ▼                    ▼                    ▼
   ┌──────────────┐     ┌──────────────┐     ┌──────────────┐
   │ RWAMarketplace│     │RentalDistributor│   │  Checkpoint   │
   │   二级市场    │     │   租金分红      │   │  历史快照查询  │
   └──────────────┘     └──────────────┘     └──────────────┘
  • RWAAsset:核心资产合约,基于 ERC-1155 实现多房产代币化发行,内置持仓快照功能,为分红提供数据支撑;

  • RWAMarketplace:去中心化交易市场,支持房产份额挂单、购买,保障交易安全与透明;

  • RentalDistributor:分红引擎,基于历史快照计算用户应得租金,支持单资产 / 多资产批量领取;

  • Checkpoint:快照模块,记录指定区块高度的用户持仓和总供应量,确保分红计算的公平性。

    • *

二、核心合约实现

2.1 测试用稳定币合约(TestUSDT)

为适配测试网环境,实现无权限铸币功能,便于快速验证合约逻辑:

//SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
 * @dev 测试网专用 USDT,任意人都能 mint
 */
contract TestUSDT is ERC20 {
    uint8 private _decimals;

    constructor(
        string memory name,
        string memory symbol,
        uint8 decimals_
    ) ERC20(name, symbol) {
        _decimals = decimals_;
    }

    function decimals() public view override returns (uint8) {
        return _decimals;
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

2.2 核心业务合约(三合一)

整合资产发行、交易、分红三大核心能力,兼顾安全性与可扩展性:

  • RWAAsset:带历史快照的ERC-1155资产合约
  • RWAMarketplace:去中心化房产份额交易
  • RentalDistributor:基于快照的租金分红引擎
    
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol"; /**

  • @title RWA房产资产合约 */ contract RWAAsset is ERC1155, ERC1155Supply, AccessControl { using Checkpoints for Checkpoints.Trace224;

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    mapping(uint256 => Checkpoints.Trace224) private _totalCheckpoints; mapping(uint256 => mapping(address => Checkpoints.Trace224)) private _checkpoints;

    constructor() ERC1155("https://api.rwa.com{id}.json") { _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); }

    function mint(address to, uint256 id, uint256 amount) external onlyRole(MINTER_ROLE) { _mint(to, id, amount, ""); }

    function getPastBalanceOf(address account, uint256 id, uint32 blockNumber) public view returns (uint256) {
    // blockNumber 必须小于当前区块
    require(blockNumber < block.number, "RWA: Future block");
    
    // 在 OZ V5 中,使用 upperLookupRecent 获取历史快照值
    return _checkpoints[id][account].upperLookupRecent(blockNumber);

    }

    function getPastTotalSupply(uint256 id, uint32 blockNumber) public view returns (uint256) { require(blockNumber < block.number, "RWA: Future block");

    // 同样替换为 upperLookupRecent
    return _totalCheckpoints[id].upperLookupRecent(blockNumber);

    }

    // 核心修复:重写 _update 以包含 ERC1155Supply 的逻辑 function _update(address from, address to, uint256[] memory ids, uint256[] memory values) internal override(ERC1155, ERC1155Supply) { // 先调用父类更新逻辑 super._update(from, to, ids, values);

    uint32 currentBlock = uint32(block.number);
    for (uint256 i = 0; i &lt; ids.length; ++i) {
        uint256 id = ids[i];
    
        // 记录用户持仓
        if (from != address(0)) {
            Checkpoints.push(_checkpoints[id][from], currentBlock, uint224(balanceOf(from, id)));
        }
        if (to != address(0)) {
            Checkpoints.push(_checkpoints[id][to], currentBlock, uint224(balanceOf(to, id)));
        }
    
        // 核心修复点:调用 ERC1155Supply 提供的 totalSupply(id)
        // 并在每次变动时强制写入总供应量快照
        Checkpoints.push(_totalCheckpoints[id], currentBlock, uint224(totalSupply(id)));
    }

    }

    // 必须重写支持接口 function supportsInterface(bytes4 interfaceId) public view override(ERC1155, AccessControl) returns (bool) { return super.supportsInterface(interfaceId); } }

contract RWAMarketplace is ReentrancyGuard { using SafeERC20 for IERC20; struct Listing { address seller; uint256 propertyId; uint256 amount; uint256 pricePerShare; bool active; } RWAAsset public immutable asset; IERC20 public immutable paymentToken; mapping(uint256 => Listing) public listings; uint256 public nextListingId; constructor(address _asset, address _paymentToken) { asset = RWAAsset(_asset); paymentToken = IERC20(_paymentToken); } function list(uint256 propertyId, uint256 amount, uint256 price) external { asset.safeTransferFrom(msg.sender, address(this), propertyId, amount, ""); listings[nextListingId] = Listing(msg.sender, propertyId, amount, price, true); nextListingId++; } function buy(uint256 listingId, uint256 buyAmount) external nonReentrant { Listing storage l = listings[listingId]; uint256 cost = buyAmount * l.pricePerShare; l.amount -= buyAmount; if(l.amount == 0) l.active = false; paymentToken.safeTransferFrom(msg.sender, l.seller, cost); asset.safeTransferFrom(address(this), msg.sender, l.propertyId, buyAmount, ""); } function onERC1155Received(address, address, uint256, uint256, bytes memory) public pure returns (bytes4) { return this.onERC1155Received.selector; } }

contract RentalDistributor is AccessControl,ReentrancyGuard { using SafeERC20 for IERC20; RWAAsset public asset; IERC20 public rewardToken; struct Distribution { uint32 snapshotBlock; uint256 amountPerShare; } mapping(uint256 => uint256) public currentDistId; mapping(uint256 => mapping(uint256 => Distribution)) public history; mapping(uint256 => mapping(address => uint256)) public userLastClaimed; constructor(address _asset, address _rewardToken) { asset = RWAAsset(_asset); rewardToken = IERC20(_rewardToken); _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); } function distribute(uint256 propertyId, uint256 amount, uint32 snapBlock) external onlyRole(DEFAULT_ADMIN_ROLE) { rewardToken.safeTransferFrom(msg.sender, address(this), amount); uint256 totalAtSnap = asset.getPastTotalSupply(propertyId, snapBlock);

// 增加此校验
require(totalAtSnap > 0, "RWA: Total supply at snapshot is zero");

uint256 id = ++currentDistId[propertyId];
history[propertyId][id] = Distribution(snapBlock, (amount * 1e12) / totalAtSnap);

} // 1. 新增:批量领取函数 function batchClaim(uint256[] calldata propertyIds) external nonReentrant { uint256 totalBatchReward = 0;

    for (uint256 i = 0; i &lt; propertyIds.length; i++) {
        uint256 pId = propertyIds[i];
        totalBatchReward += _calculateAndUpdateReward(pId, msg.sender);
    }

    require(totalBatchReward > 0, "RWA: No rewards to claim");
    rewardToken.safeTransfer(msg.sender, totalBatchReward);
}

// 2. 内部逻辑:计算并更新用户的领取状态
function _calculateAndUpdateReward(uint256 propertyId, address user) internal returns (uint256) {
    uint256 last = userLastClaimed[propertyId][user];
    uint256 current = currentDistId[propertyId];

    if (last >= current) return 0;

    uint256 reward = 0;
    for (uint256 i = last + 1; i &lt;= current; i++) {
        Distribution storage d = history[propertyId][i];
        uint256 bal = asset.getPastBalanceOf(user, propertyId, d.snapshotBlock);
        reward += (bal * d.amountPerShare) / 1e12;
    }

    userLastClaimed[propertyId][user] = current;
    return reward;
}

// 3. 保留单体领取(调用内部逻辑)
function claim(uint256 propertyId) external nonReentrant {
    uint256 reward = _calculateAndUpdateReward(propertyId, msg.sender);
    require(reward > 0, "RWA: No reward");
    rewardToken.safeTransfer(msg.sender, reward);
}
// function claim(uint256 propertyId) external { uint256 last = userLastClaimed[propertyId][msg.sender]; uint256 current = currentDistId[propertyId]; uint256 totalReward = 0; for (uint256 i = last + 1; i &lt;= current; i++) { Distribution storage d = history[propertyId][i]; uint256 bal = asset.getPastBalanceOf(msg.sender, propertyId, d.snapshotBlock); totalReward += (bal * d.amountPerShare) / 1e12; } userLastClaimed[propertyId][msg.sender] = current; if (totalReward > 0) rewardToken.safeTransfer(msg.sender, totalReward); }

}

# 测试脚本
**测试用例:房产RWA交易流程完整测试**
 * **✔ 应该成功运行完整的 铸造->交易->快照->分红 流程**
 * **✔ 应该支持跨多个房产资产的一次性批量领取 (Batch Claim)**
 * **✔ 应该能够累积多次分红后一次性领取**
 * **✔ 如果资产在快照后发生转移,分红应归属于快照时的持有者**

import assert from "node:assert/strict"; import { describe, it, beforeEach } from "node:test"; import { network } from "hardhat"; import { parseUnits,} from "viem";

describe("房产RWA交易流程完整测试", function () { let asset: any, market: any, dist: any, usdc: any; let admin: any, alice: any, bob: any; let publicClient: any; let testClient: any; // 用于手动挖矿 const PROP_ID = 1n; const PROP_A = 101n; // 使用不同的 ID 防止冲突 const PROP_B = 102n; beforeEach(async function () { const { viem } = await (network as any).connect();

    // 1. 获取客户端
    publicClient = await viem.getPublicClient();
    [admin, alice, bob] = await viem.getWalletClients();

    // 2. 初始化 Test Client 用于控制区块链高度 (mine)
    testClient = await viem.getTestClient();

    // 3. 部署合约 (确保你已经写好了 MockERC20)
    usdc = await viem.deployContract("TestUSDT", ["USDC", "USDC",18]); 
    asset = await viem.deployContract("RWAAsset", []);
    market = await viem.deployContract("RWAMarketplace", [asset.address, usdc.address]);
    dist = await viem.deployContract("RentalDistributor", [asset.address, usdc.address]);

    const MINTER = await asset.read.MINTER_ROLE();
    await asset.write.grantRole([MINTER, admin.account.address]);
});

it("应该成功运行完整的 铸造->交易->快照->分红 流程", async function () {
    // --- 1. 初始状态:区块 N ---
    await asset.write.mint([alice.account.address, PROP_ID, 1000n]);

    // --- 2. 交易状态:区块 N+1, N+2 ---
    await asset.write.setApprovalForAll([market.address, true], { account: alice.account });
    await market.write.list([PROP_ID, 500n, parseUnits("10", 18)], { account: alice.account });

    await usdc.write.mint([bob.account.address, parseUnits("5000", 18)]);
    await usdc.write.approve([market.address, parseUnits("5000", 18)], { account: bob.account });

    // 这一步买入操作执行在区块 M
    await market.write.buy([0n, 200n], { account: bob.account });

    // --- 核心修复点:确保快照已封存 ---
    // 1. 先挖一个新块,确保上面的 buy 操作所在的区块已经完全“结束”并写回快照
    await testClient.mine({ blocks: 1 });

    // 2. 现在获取“上一个区块”作为快照基准块
    const lastBlock = await publicClient.getBlockNumber();
    const snapBlock = Number(lastBlock) - 1; 

    // 3. 再次手动挖矿,确保当前区块高度 > snapBlock (查询要求)
    await testClient.mine({ blocks: 1 });

    // 4. 调试打印:验证供应量是否已记录
    const ts = await asset.read.getPastTotalSupply([PROP_ID, snapBlock]);
    console.log(`基准区块: ${snapBlock}, 该区块供应量: ${ts}`);
    assert.ok(ts > 0n, "供应量应大于0");

    // --- 3. 分发租金 ---
    // 建议统一使用 18 位或根据部署参数对齐
    const rent = parseUnits("10000", 18); 
    await usdc.write.mint([admin.account.address, rent]);
    await usdc.write.approve([dist.address, rent]);

    // 执行分红
    await dist.write.distribute([PROP_ID, rent, snapBlock]);

    // --- 4. 验证分红 ---
    await dist.write.claim([PROP_ID], { account: alice.account });
    const aliceBal = await usdc.read.balanceOf([alice.account.address]);
    // 如果原本 Alice 卖 100 块赚了钱,这里断言需要加上初始卖房所得,或者只查分红增量
    console.log(`Alice 余额: ${aliceBal}`);

    await dist.write.claim([PROP_ID], { account: bob.account });
    const bobBal = await usdc.read.balanceOf([bob.account.address]);
    console.log(`Bob 余额: ${bobBal}`);

    assert.ok(bobBal > 0n, "Bob 应该领到收益");
});
it("应该支持跨多个房产资产的一次性批量领取 (Batch Claim)", async function () {
        // 确保在这个测试实例中 Alice 有钱
        await asset.write.mint([alice.account.address, PROP_A, 1000n]);
        await asset.write.mint([alice.account.address, PROP_B, 2000n]);
        await testClient.mine({ blocks: 1 });

        const snapBlock = Number(await publicClient.getBlockNumber());
        await testClient.mine({ blocks: 1 });

        const rentA = parseUnits("5000", 18);
        const rentB = parseUnits("12000", 18);
        await usdc.write.mint([admin.account.address, rentA + rentB]);
        await usdc.write.approve([dist.address, rentA + rentB]);

        await dist.write.distribute([PROP_A, rentA, snapBlock]);
        await dist.write.distribute([PROP_B, rentB, snapBlock]);

        const initBal = await usdc.read.balanceOf([alice.account.address]);
        await dist.write.batchClaim([[PROP_A, PROP_B]], { account: alice.account });

        const endBal = await usdc.read.balanceOf([alice.account.address]);
        assert.equal(endBal - initBal, rentA + rentB);
 });

it("应该能够累积多次分红后一次性领取", async function () {
        // 1. 铸造并确保快照记录了供应量
        await asset.write.mint([alice.account.address, PROP_A, 1000n]);
        await testClient.mine({ blocks: 1 }); 

        // 2. 第一次分红
        const snap1 = Number(await publicClient.getBlockNumber());
        await testClient.mine({ blocks: 1 });
        const rent1 = parseUnits("1000", 18);
        await usdc.write.mint([admin.account.address, rent1]);
        await usdc.write.approve([dist.address, rent1]);
        await dist.write.distribute([PROP_A, rent1, snap1]);

        // 3. 第二次分红
        await testClient.mine({ blocks: 2 });
        const snap2 = Number(await publicClient.getBlockNumber());
        await testClient.mine({ blocks: 1 });
        const rent2 = parseUnits("2000", 18);
        await usdc.write.mint([admin.account.address, rent2]);
        await usdc.write.approve([dist.address, rent2]);
        await dist.write.distribute([PROP_A, rent2, snap2]);

        // 4. 领取
        const initBal = await usdc.read.balanceOf([alice.account.address]);
        await dist.write.claim([PROP_A], { account: alice.account });

        const endBal = await usdc.read.balanceOf([alice.account.address]);
        assert.ok(endBal - initBal >= parseUnits("3000", 18));
});

it("如果资产在快照后发生转移,分红应归属于快照时的持有者", async function () {
        // 1. 初始准备
        await asset.write.mint([alice.account.address, PROP_A, 1000n]);
        await testClient.mine({ blocks: 1 });

        const snapBeforeTransfer = Number(await publicClient.getBlockNumber());
        await testClient.mine({ blocks: 1 });

        // 2. Alice 转移给 Bob
        await asset.write.safeTransferFrom([
            alice.account.address, 
            bob.account.address, 
            PROP_A, 
            1000n, 
            "0x"
        ], { account: alice.account });
        await testClient.mine({ blocks: 1 });

        // 3. 执行分红(基于转移前的快照)
        const rent = parseUnits("1000", 18);
        await usdc.write.mint([admin.account.address, rent]);
        await usdc.write.approve([dist.address, rent]);
        await dist.write.distribute([PROP_A, rent, snapBeforeTransfer]);

        // 4. Alice 领取(她在快照时是持有者)
        await dist.write.claim([PROP_A], { account: alice.account });
        const aliceBal = await usdc.read.balanceOf([alice.account.address]);
        assert.ok(aliceBal > 0n);

        // 5. Bob 尝试领取应失败
        await assert.rejects(
            dist.write.claim([PROP_A], { account: bob.account }),
            /RWA: No reward/
        );
});

});

# 部署脚本

// 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 USDC_DECIMALS = 18; const USDCAfter=await artifacts.readArtifact("TestUSDT"); const RWAAssetArtifact = await artifacts.readArtifact("RWAAsset"); const RWAMarketplaceArtifact = await artifacts.readArtifact("RWAMarketplace"); const RentalDistributorArtifact = await artifacts.readArtifact("RentalDistributor"); const USDCHash=await deployer.deployContract({ abi: USDCAfter.abi,//获取abi bytecode: USDCAfter.bytecode,//硬编码 args: ["USDC", "USDC",USDC_DECIMALS] }); const USDCReceipt=await publicClient.waitForTransactionReceipt({ hash : USDCHash }); console.log("USDC合约地址:", USDCReceipt.contractAddress); // 部署(构造函数参数:recipient, initialOwner) const RWAAssetHash = await deployer.deployContract({ abi: RWAAssetArtifact.abi,//获取abi bytecode: RWAAssetArtifact.bytecode,//硬编码 args: [],//USDC合约地址 });

// 等待确认并打印地址 const RWAAssetReceipt = await publicClient.waitForTransactionReceipt({ hash : RWAAssetHash }); console.log("RWAAsset合约地址:", RWAAssetReceipt.contractAddress); const RWAMarketplaceHash = await deployer.deployContract({ abi: RWAMarketplaceArtifact.abi,//获取abi bytecode: RWAMarketplaceArtifact.bytecode,//硬编码 args: [RWAAssetReceipt.contractAddress,USDCReceipt.contractAddress],//USDC合约地址 }); const RWAMarketplaceReceipt = await publicClient.waitForTransactionReceipt({ hash : RWAMarketplaceHash }); console.log("RWAMarketplace合约地址:", RWAMarketplaceReceipt.contractAddress); const RentalDistributorHash = await deployer.deployContract({ abi: RentalDistributorArtifact.abi, bytecode: RentalDistributorArtifact.bytecode, args: [RWAMarketplaceReceipt.contractAddress,USDCReceipt.contractAddress], }); const RentalDistributorReceipt = await publicClient.waitForTransactionReceipt({ hash: RentalDistributorHash }); console.log("RentalDistributor合约地址:", RentalDistributorReceipt.contractAddress);

}

main().catch(console.error);


# 总结
至此,关于构建链上房产RWA系统的最小可行单元(MVP)已全部落地。本文从架构设计出发,完整梳理了基于OpenZeppelin V5与Solidity 0.8.24的合约开发、Hardhat+Viem的测试验证、以及自动化部署的全流程实践,为RWA资产上链提供了可复用的技术范式与工程参考。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
木西
木西
0x5D5C...2dD7
江湖只有他的大名,没有他的介绍。