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

4 阅读11分钟

前言

随着区块链技术与真实世界资产(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-20ERC-721ERC-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 < 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 < 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 <= 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 <= 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资产上链提供了可复用的技术范式与工程参考。