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

  • 木西
  • 发布于 7小时前
  • 阅读 85

前言30行Solidity,把一间收租公寓变成链上印钞机:完整开发、部署、测试实战;【业务场景全景图】角色:托管人(房东/资管公司)、投资人、租客、链上合约资产上链•托管人拥有一套月租3000USDC的公寓,估值30万USDC。•在链上一次性铸造1000

前言

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版本不同也会有些许差异;
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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