前言30行Solidity,把一间收租公寓变成链上印钞机:完整开发、部署、测试实战;【业务场景全景图】角色:托管人(房东/资管公司)、投资人、租客、链上合约资产上链•托管人拥有一套月租3000USDC的公寓,估值30万USDC。•在链上一次性铸造1000
30 行 Solidity,把一间收租公寓变成链上印钞机:完整开发、部署、测试实战;
角色:托管人(房东/资管公司)、投资人、租客、链上合约
reportRent(3 000e6)
。claimable
,再完成转账。claim()
把累积的 USDC 提到钱包。onlyOwner
限托管人上传租金,防止恶意增发收益。说明:基于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";
/**
发行 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);
/**
/ ----------------------------------------------------
/**
托管人把链下租金打入合约并记录 / 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版本不同也会有些许差异;
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!