30 行 Solidity 把现实房租变成 USDC 现金流:RWA 版「链上乐高」

24 阅读6分钟

前言

30 行 Solidity,把一间收租公寓变成链上印钞机:完整开发、部署、测试实战;

【业务场景全景图】

角色:托管人(房东/资管公司)、投资人、租客、链上合约

  1. 资产上链
    • 托管人拥有一套月租 3 000 USDC 的公寓,估值 30 万 USDC。
    • 在链上一次性铸造 1 000 枚 RWAAPT(1 枚 = 0.1 % 房产权益)。
    • 托管人保留 100 枚作为自留权益,其余 900 枚通过 DEX/OTC 卖给投资人。
  2. **租金现金流 ** • 每月 1 号,托管人把租客线下支付的 3 000 USDC 打进合约 reportRent(3 000e6)
    • 合约立即按持币比例空投:每枚 RWAAPT 获得 3 USDC。
    – Alice 持有 400 枚 → 直接可领取 1 200 USDC。
    – Bob 持有 200 枚 → 可领取 600 USDC。
  3. 二级市场交易
    • 投资人可随时在 Uniswap V3 上买卖 RWAAPT,价格随租金收益率实时波动。
    • 转账时自动结算利息:
    – Alice 把 100 枚转给 Bob,系统先把她的 300 USDC 利息写进 claimable,再完成转账。
    – Bob 立即多拿到 300 USDC 的「待领收益」。
  4. 退出与赎回
    • 托管人可发起「回购」:在二级市场按市场价回购 RWAAPT 并销毁,实现链下房产再整合。
    • 投资人也可随时 claim() 把累积的 USDC 提到钱包。
  5. 合规与风控 • 合约 onlyOwner 限托管人上传租金,防止恶意增发收益。
    • 链下 SPV 托管房产产权,链上通证与法律文件 1:1 对应,满足 RWA 合规审计要求

开发

代币代码

说明:基于Openzeppelin ERC20代币标准,添加了铸造方法

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

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

contract MyToken is ERC20, ERC20Permit {
    constructor() ERC20("Real-World Apt Token", "RWAAPT") ERC20Permit("RWAAPT") {
        _mint(msg.sender, 1000e18);
    }
    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }

}

核心代码

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

/* ====== OpenZeppelin 5.x imports ====== */
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title RWAAPT
 * @dev 把一套月租公寓映射到链上
 *      发行 1 000 枚 ERC-20 通证,租金按持币比例空投 USDC
 */
contract RWAAPT is ERC20, Ownable {
    IERC20 public immutable USDC;
    uint256 public constant TOTAL_SUPPLY = 1_000 * 1e18;

    // 累计 USDC / token (放大了 1e18)
    uint256 public cumulativeUsdcPerToken;

    // 用户 => 上次结算时的 cumulativeUsdcPerToken
    mapping(address => uint256) public userSnapshot;

    // 已分配但未领取
    mapping(address => uint256) public claimable;

    event RentDistributed(uint256 amount);
    event RentClaimed(address indexed user, uint256 amount);

    /**
     * @param _usdc 链上 USDC 地址
     */
    constructor(address _usdc) ERC20("Real-World Apt Token", "RWAAPT") Ownable(msg.sender) {
        USDC = IERC20(_usdc);
        _mint(msg.sender, TOTAL_SUPPLY); // 一次性全部铸给部署者
    }

    /* ---------------------------------------------------- *
     *                      外部函数                        *
     * ---------------------------------------------------- */

    /**
     * 托管人把链下租金打入合约并记录
     */
    function reportRent(uint256 usdcAmount) external onlyOwner {
        require(usdcAmount > 0, "Zero rent");
        // 托管人先 approve
        USDC.transferFrom(msg.sender, address(this), usdcAmount);
        uint256 addedPerToken = (usdcAmount * 1e18) / TOTAL_SUPPLY;
        cumulativeUsdcPerToken += addedPerToken;

        emit RentDistributed(usdcAmount);
    }

    /**
     * 投资人领取已分配的 USDC
     */
    function claim() external {
        _updateClaimable(msg.sender);

        uint256 amount = claimable[msg.sender];
        require(amount > 0, "Nothing to claim");

        claimable[msg.sender] = 0;
        USDC.transfer(msg.sender, amount);

        emit RentClaimed(msg.sender, amount);
    }

    /* ---------------------------------------------------- *
     *                      内部函数                        *
     * ---------------------------------------------------- */

    /**
     * 更新某地址可领取的 USDC
     */
    function _updateClaimable(address user) internal {
        uint256 owed = (balanceOf(user) *
            (cumulativeUsdcPerToken - userSnapshot[user])) / 1e18;

        claimable[user] += owed;
        userSnapshot[user] = cumulativeUsdcPerToken;
    }

    /**
     * 重载 _updateClaimable 在转账时自动触发
     */
    function _update(address from, address to, uint256 value)
        internal
        override(ERC20)
    {
        super._update(from, to, value); // 先执行 ERC20 本身的逻辑

        if (from != address(0)) _updateClaimable(from);
        if (to   != address(0)) _updateClaimable(to);
    }
}

测试

说明:针对核心代码测试,测试流程如下:托管人分发租金后,用户可按比例领取 USDC、转账后利息自动结算给双方、重复领取不会多给等场景

const {
  time,
  loadFixture,
} =require("@nomicfoundation/hardhat-toolbox/network-helpers");
const { expect } = require("chai");
const { ethers } = require("hardhat");
// import { RWAAPT, MockUSDC } from "../typechain-types";

describe("RWAAPT", function () {
  /* ---------------------------------------------------- *
   *                      公共夹具                         *
   * ---------------------------------------------------- */
  async function deployFixture() {
    const [deployer, alice, bob] = await ethers.getSigners();

    // 1. 部署一个简化版 USDC(18 位精度,方便测试)
    const MockUSDC = await ethers.getContractFactory("MyToken");
    const usdc = (await MockUSDC.deploy());
    await usdc.waitForDeployment();

    // 2. 部署 RWAAPT
    const RWAAPT = await ethers.getContractFactory("RWAAPT");
    const rwaapt = (await RWAAPT.deploy(await usdc.getAddress()));
    await rwaapt.waitForDeployment();

    // 3. 给 Alice 和 Bob 各分 400 token,其余 200 留在 deployer
    await rwaapt.transfer(alice.address, ethers.parseEther("400"));
    await rwaapt.transfer(bob.address, ethers.parseEther("400"));

    return { rwaapt, usdc, deployer, alice, bob };
  }

  /* ---------------------------------------------------- *
   *                        测试用例                        *
   * ---------------------------------------------------- */

  describe("Deployment", () => {
    it("总供应量 1000,且全部在部署者", async () => {
      const { rwaapt, deployer } = await loadFixture(deployFixture);
      expect(await rwaapt.TOTAL_SUPPLY()).to.equal(ethers.parseEther("1000"));
      expect(await rwaapt.totalSupply()).to.equal(ethers.parseEther("1000"));
      expect(await rwaapt.balanceOf(deployer.address)).to.equal(
        ethers.parseEther("200")
      ); // 1000 - 400 - 400
    });
  });

  describe("reportRent / claim", () => {
  it("托管人分发租金后,用户可按比例领取 USDC", async () => {
    const { rwaapt, usdc, deployer, alice, bob } = await loadFixture(deployFixture);

    // 1. mint & approve
    await usdc.mint(deployer.address, 10_000 * 10 ** 6);
    await usdc.connect(deployer).approve(await rwaapt.getAddress(), 10_000 * 10 ** 6);

    // 2. reportRent
    await expect(rwaapt.connect(deployer).reportRent(6000 * 10 ** 6))
      .to.emit(rwaapt, "RentDistributed")
      .withArgs(6000 * 10 ** 6);

    // 3. 触发 alice 的结算(可选:先读一次 claimable,或直接用 claim)
    // 这里直接 claim 即可
    await expect(rwaapt.connect(alice).claim())
      .to.emit(rwaapt, "RentClaimed")
      .withArgs(alice.address, 2400 * 10 ** 6);

    // 4. 断言 USDC 余额
    expect(await usdc.balanceOf(alice.address)).to.equal(2400 * 10 ** 6);
  });
});

  describe("Transfer & interest sync", () => {
  it("转账后利息自动结算给双方", async () => {
    const { rwaapt, usdc, deployer, alice, bob } = await loadFixture(deployFixture);

    // 1. 托管人打入 3000 USDC 租金
    await usdc.mint(deployer.address, 3000 * 10 ** 6);
    await usdc.connect(deployer).approve(await rwaapt.getAddress(), 3000 * 10 ** 6);
    await rwaapt.connect(deployer).reportRent(3000 * 10 ** 6);

    // 2. 触发 alice 的结算(零额度转账给自己即可)
    await rwaapt.connect(alice).transfer(alice.address, 0);

    // 3. 此时 alice 的 1200 USDC 已写入 claimable
    expect(await rwaapt.claimable(alice.address)).to.equal(1200 * 10 ** 6);

    // 4. Alice 再转 100 token 给 Bob
    await rwaapt.connect(alice).transfer(bob.address, ethers.parseEther("100"));

    // 5. 触发 bob 的结算(同样零额度转账给自己)
    await rwaapt.connect(bob).transfer(bob.address, 0);

    // 6. Bob 应有 400->500 token,累计利息 1500 USDC
    expect(await rwaapt.claimable(bob.address)).to.equal(1500 * 10 ** 6);
  });
});

describe("Edge cases", () => {
  it("重复领取不会多给", async () => {
    const { rwaapt, usdc, deployer, alice } = await loadFixture(deployFixture);

    // 1. 第一次租金 1000 USDC
    await usdc.mint(deployer.address, 1000 * 10 ** 6);
    await usdc.connect(deployer).approve(await rwaapt.getAddress(), 1000 * 10 ** 6);
    await rwaapt.connect(deployer).reportRent(1000 * 10 ** 6);

    // 2. alice 第一次领取
    await rwaapt.connect(alice).claim();
    expect(await rwaapt.claimable(alice.address)).to.equal(0);

    // 3. 第二次租金 1000 USDC
    await usdc.connect(deployer).approve(await rwaapt.getAddress(), 1000 * 10 ** 6);
    await rwaapt.connect(deployer).reportRent(1000 * 10 ** 6);

    // 4. 强制触发结算(零额度转账给自己)
    await rwaapt.connect(alice).transfer(alice.address, 0);

    // 5. 此时 alice 应有 400 * 1 = 400 USDC 新增利息
    expect(await rwaapt.claimable(alice.address)).to.equal(400 * 10 ** 6);
  });
});
});

部署

注意部署核心代码时要严格按照合约执行顺序,代币合约在RWA合约之前

代币合约

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy } = deployments;
  const getNamedAccount = (await getNamedAccounts()).firstAccount;
 const myToken=await deploy("MyToken", {
    from: getNamedAccount,
    args: [],//部署时要传的参数
    log: true,
  });
  console.log("MyToken deployed at:", myToken.address);
};
// 部署治理 npx hardhat deploy --tags myToken 指定的部署文件 部署的文件包是Deploy
module.exports.tags = ["all", "myToken"];
RWA合约
module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy } = deployments;
  const getNamedAccount = (await getNamedAccounts()).firstAccount;
  const MyToken=await deployments.get("MyToken");//
 const RWAAPT=await deploy("RWAAPT", {
    from: getNamedAccount,
    args: [MyToken.address],
    log: true,
  });
  console.log("RWAAPT deployed at:", RWAAPT.address);
};
// 部署治理 npx hardhat deploy --tags RWAAPT 指定的部署文件 部署的文件包是Deploy
module.exports.tags = ["all", "RWAAPT"];

总结

以上就是具体场景落地的全部过程,主要在部署合约中的执行顺序,使用Openzeppelin版本不同也会有些许差异;