前言
30 行 Solidity,把一间收租公寓变成链上印钞机:完整开发、部署、测试实战;
【业务场景全景图】
角色:托管人(房东/资管公司)、投资人、租客、链上合约
- 资产上链
• 托管人拥有一套月租 3 000 USDC 的公寓,估值 30 万 USDC。
• 在链上一次性铸造 1 000 枚 RWAAPT(1 枚 = 0.1 % 房产权益)。
• 托管人保留 100 枚作为自留权益,其余 900 枚通过 DEX/OTC 卖给投资人。 - **租金现金流 **
• 每月 1 号,托管人把租客线下支付的 3 000 USDC 打进合约
reportRent(3 000e6)。
• 合约立即按持币比例空投:每枚 RWAAPT 获得 3 USDC。
– Alice 持有 400 枚 → 直接可领取 1 200 USDC。
– Bob 持有 200 枚 → 可领取 600 USDC。 - 二级市场交易
• 投资人可随时在 Uniswap V3 上买卖 RWAAPT,价格随租金收益率实时波动。
• 转账时自动结算利息:
– Alice 把 100 枚转给 Bob,系统先把她的 300 USDC 利息写进claimable,再完成转账。
– Bob 立即多拿到 300 USDC 的「待领收益」。 - 退出与赎回
• 托管人可发起「回购」:在二级市场按市场价回购 RWAAPT 并销毁,实现链下房产再整合。
• 投资人也可随时claim()把累积的 USDC 提到钱包。 - 合规与风控
• 合约
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版本不同也会有些许差异;