前言
随着分布式网络技术的成熟,可编程数字资产的年结算规模已正式迈入数十万亿美元的量级。在这种背景下,PayFi(可编程智能清算,Payment Finance) 概念迅速崛起。不同于传统的电子支付仅仅把数据作为“交易媒介”,PayFi 的核心逻辑在于利用代码重构资金的时间效率(TVM),将链上自动化清算逻辑、分布式存管流转与日常高频结算深度融合。
关于 PayFi 的核心定义、技术演进以及宏观系统理论相关的知识,读者可以去查看我们的上一篇文章,其中有非常详细的背景解释与理论说明,本文在此便不再赘述。为了让开发者更直观地感受可编程代码的魅力,本文主要关注相关代码的落地实践。
在接下来的内容中,我们将基于 Solidity 0.8.27 与 OpenZeppelin v5 标准,直接通过工程视角实作一个颠覆传统清算逻辑的 PayFi 创新应用:“本金锁定、收益实时清算” 的智能合约架构。
一、 什么是 PayFi?其技术核心是什么?
传统 Web2 支付(如 Visa、Mastercard)本质上是基于“结算账期”的延迟清算机制。而 Web3 的 PayFi 则是将生息资产与可编程合约相结合。
在 PayFi 生态中,当你将一笔资金存入钱包,这笔资金不会像在传统银行或 Web3 普通钱包中静止不动,而是会自动接入美债、货币市场基金等 RWA 生息协议。当你产生消费时,智能合约会以秒为单位实时计算你的资金利息,并优先扣除利息部分向商家进行清算,从而确保你的“本金毫发无损”。
二、 架构设计:生息支付合约实现
以下合约实现了 PayFi 的核心结算逻辑:用户储值本金(MUSD 稳定币),合约根据时间精确计算收益。当用户向商家发起支付 payMerchant 时,系统会优先扣除该用户积累的利息。
1. 核心智能合约 (PayFiYieldPayment.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
// 引入 OpenZeppelin v5 最新標準合約
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable2Step.sol"; // v5 推薦使用更安全的雙重驗證
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
/**
* @title PayFiYieldPayment
* @dev 實現「本金不動,利息消費」的 PayFi 核心支付合約
*/
contract PayFiYieldPayment is Ownable2Step, ReentrancyGuard {
using SafeERC20 for IERC20;
// 核心資產:模擬生息穩定幣(例如 USDC 或 RWA 美債代幣,此處以底層 ERC20 代幣為基礎)
IERC20 public immutable paymentToken;
// 模擬年化收益率(APY),例如 500 代表 5% (基數為 10000)
uint256 public constant APY_BASE = 10000;
uint256 public yieldAPY = 500;
struct UserAccount {
uint256 principal; // 存入的本金
uint256 lastDepositTime;// 上次更新或存入的時間(用於計算複利/利息)
uint256 accumulatedYield;// 已結算但未使用的利息餘額
}
mapping(address => UserAccount) public users;
// 事件宣告
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 principalAmount);
event PaymentMade(address indexed user, address indexed merchant, uint256 amount, bool usedPrincipal);
event APYUpdated(uint256 newAPY);
/**
* @dev 構造函數,初始化代幣與權限
* @param _tokenAddress 底層支付穩定幣地址
*/
constructor(address _tokenAddress) Ownable(msg.sender) {
require(_tokenAddress != address(0), "Invalid token address");
paymentToken = IERC20(_tokenAddress);
}
/**
* @notice 用戶存入本金以生成未來支付的利息
* @param _amount 存入的代幣數量
*/
function deposit(uint256 _amount) external nonReentrant {
require(_amount > 0, "Amount must be greater than 0");
// 1. 先結算該用戶之前的利息
_updateYield(msg.sender);
// 2. 更新本金與時間戳
users[msg.sender].principal += _amount;
users[msg.sender].lastDepositTime = block.timestamp;
// 3. 安全轉賬(OpenZeppelin v5 SafeERC20)
paymentToken.safeTransferFrom(msg.sender, address(this), _amount);
emit Deposited(msg.sender, _amount);
}
/**
* @notice 核心 PayFi 創新功能:使用利息向商家付款
* @dev 優先扣除利息。若利息不足且允許,才會扣除本金
* @param _merchant 商家地址
* @param _amount 支付金額
*/
function payMerchant(address _merchant, uint256 _amount) external nonReentrant {
require(_merchant != address(0), "Invalid merchant address");
require(_amount > 0, "Amount must be greater than 0");
// 1. 實時結算最新利息
_updateYield(msg.sender);
UserAccount storage account = users[msg.sender];
bool usedPrincipal = false;
// 2. 判斷利息是否足夠支付
if (account.accumulatedYield >= _amount) {
// 完全由利息買單(Buy Now, Pay Never! 本金毫髮無損)
account.accumulatedYield -= _amount;
} else {
// 利息不足,扣除全部剩餘利息,剩餘部分從本金扣除(或直接拒絕交易,此處選擇扣除本金)
uint256 remainingAmount = _amount - account.accumulatedYield;
account.accumulatedYield = 0;
require(account.principal >= remainingAmount, "Insufficient total balance");
account.principal -= remainingAmount;
usedPrincipal = true;
}
// 3. 更新時間戳
account.lastDepositTime = block.timestamp;
// 4. 將資金安全釋放給商家
paymentToken.safeTransfer(_merchant, _amount);
emit PaymentMade(msg.sender, _merchant, _amount, usedPrincipal);
}
/**
* @notice 用戶全額或部分贖回本金
* @param _amount 想要提取的本金金額
*/
function withdraw(uint256 _amount) external nonReentrant {
UserAccount storage account = users[msg.sender];
require(account.principal >= _amount, "Insufficient principal");
// 1. 結算當前利息
_updateYield(msg.sender);
// 2. 扣除本金並更新時間
account.principal -= _amount;
account.lastDepositTime = block.timestamp;
// 3. 轉賬返還用戶
paymentToken.safeTransfer(msg.sender, _amount);
emit Withdrawn(msg.sender, _amount);
}
/**
* @notice 查看用戶當前可用的總額度(本金 + 實時利息)
*/
function getBalanceInfo(address _user) external view returns (uint256 principal, uint256 availableYield) {
UserAccount memory account = users[_user];
uint256 pendingYield = _calculatePendingYield(_user);
return (account.principal, account.accumulatedYield + pendingYield);
}
/**
* @dev 管理員更新收益池 APY(模擬外部 RWA 收益率波動)
*/
function setAPY(uint256 _newAPY) external onlyOwner {
require(_newAPY <= 3000, "APY too high for safety"); // 限制最高 30%
yieldAPY = _newAPY;
emit APYUpdated(_newAPY);
}
/**
* @dev 內部函數:結算並更新利息到賬戶中
*/
function _updateYield(address _user) internal {
uint256 pending = _calculatePendingYield(_user);
if (pending > 0) {
users[_user].accumulatedYield += pending;
}
}
/**
* @dev 內部函數:根據時間戳精確計算未結算的利息 (貨幣的時間價值)
*/
function _calculatePendingYield(address _user) internal view returns (uint256) {
UserAccount memory account = users[_user];
if (account.principal == 0 || account.lastDepositTime == 0) {
return 0;
}
// 計算時間差(秒)
uint256 timeElapsed = block.timestamp - account.lastDepositTime;
// 利息 = 本金 * APY * 時間 / (365天以秒計算)
// 365天 = 31536000 秒
uint256 pending = (account.principal * yieldAPY * timeElapsed) / (APY_BASE * 31536000);
return pending;
}
}
2. Mock代币智能合约 (MockStablecoin.sol)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
// 引入 OpenZeppelin v5 標準 ERC20 合約
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MockStablecoin
* @dev 用於 PayFi 測試腳本的模擬穩定幣(例如模擬 USDC/USDT)
*/
contract MockStablecoin is ERC20, Ownable {
/**
* @dev 構造函數
* @param initialSupply 初始鑄造的代幣總量(包含 18 位小數)
*/
constructor(uint256 initialSupply)
ERC20("Mock Stablecoin", "MUSD")
Ownable(msg.sender)
{
// 將所有初始代幣鑄造給合約部署者,方便測試腳本分發資金
_mint(msg.sender, initialSupply);
}
/**
* @notice 額外提供一個鑄造接口(可選)
* @dev 方便測試腳本在測試中途隨時為特定用戶「憑空」印鈔
* @param to 接收代幣的地址
* @param amount 鑄造數量
*/
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
/**
* @dev 模擬穩定幣通常使用 6 位小數(如真實 USDC/USDT)
* @dev 為了配合您測試腳本中的 `parseEther` (預設 18 位),此處保留 18 位。
* @dev 如果想完全模擬 USDC,可取消下方註釋將其改為 6 位(此時腳本需同步改為 parseUnits)。
*/
/*
function decimals() public view virtual override returns (uint8) {
return 6;
}
*/
}
三、 开发、调试与时间旅行测试
在测试 PayFi 合约时,最核心的痛点在于如何模拟时间流逝以验证利息收益。利用 Viem 测试客户端 (TestClient) ,我们可以在 Hardhat 节点环境下进行“时间旅行(Time Travel)”,从而测试高精度、动态的时间金融逻辑。
以下是基于 viem、node:test 和 node:assert/strict 实现的完整工程整合测试脚本。
2. 集成测试脚本 (PayFi.ts)
- PayFi Yield Payment Protocol Test Suite
- 基礎儲值驗證:用戶應能成功存入本金且賬本記錄正確
- 時間價值驗證:隨時間推移應能精確產生並累加利息額度
- PayFi 核心支付:當利息充足時,應實現「本金不動,利息消費」
- 異常邊界處理:當利息不足以支付時,應自動扣除本金補足
- 贖回機制驗證:用戶隨時可提現全部或部分本金
- 權限與治理拦截:非合約 Owner 無權調整收益池 APY
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { parseEther, getAddress } from "viem";
import { network } from "hardhat";
describe("PayFi Yield Payment Protocol Test Suite", function () {
// 部署 Fixture 基礎環境
async function deployFixture() {
// 獲取 Hardhat 的 viem 客戶端
const { viem } = await (network as any).connect();
const [owner, user, merchant] = await viem.getWalletClients();
const publicClient = await viem.getPublicClient();
// 🔥 【關鍵修復】獲取 Viem 的測試客戶端,用於操縱時間和區塊
const testClient = await viem.getTestClient();
// 1. 部署一個標準 ERC20 代幣作為模擬穩定幣
const mockUSDC = await viem.deployContract("MockStablecoin", [
parseEther("1000000"),
]);
// 2. 部署 PayFi 核心支付合約
const payFiContract = await viem.deployContract("PayFiYieldPayment", [
mockUSDC.address,
]);
// 3. 初始化資金:分發 10,000 USDC 給測試用戶
const initialUserBalance = parseEther("10000");
await mockUSDC.write.transfer([user.account.address, initialUserBalance], {
account: owner.account,
});
// 用戶授權給 PayFi 合約
await mockUSDC.write.approve([payFiContract.address, parseEther("1000000")], {
account: user.account,
});
return {
mockUSDC,
payFiContract,
owner,
user,
merchant,
publicClient,
testClient, // 導出 testClient 供後續測試使用
};
}
it("基礎儲值驗證:用戶應能成功存入本金且賬本記錄正確", async function () {
const { payFiContract, mockUSDC, user } = await deployFixture();
const depositAmount = parseEther("1000");
await payFiContract.write.deposit([depositAmount], {
account: user.account,
});
const [principal, availableYield] = await payFiContract.read.getBalanceInfo([
user.account.address,
]);
assert.equal(principal, depositAmount, "存入的本金數量不匹配");
assert.equal(availableYield, 0n, "剛存入時不應有利息產生");
const contractBalance = await mockUSDC.read.balanceOf([payFiContract.address]);
assert.equal(contractBalance, depositAmount, "合約未收到對應代幣");
});
it("時間價值驗證:隨時間推移應能精確產生並累加利息額度", async function () {
const { payFiContract, user, testClient } = await deployFixture();
const depositAmount = parseEther("10000");
await payFiContract.write.deposit([depositAmount], {
account: user.account,
});
// 🔥 【關鍵修復】使用 Viem 內置的 testClient 精確推進時間 1 年並挖礦
await testClient.increaseTime({ seconds: 31536000 });
await testClient.mine({ blocks: 1 });
const [principal, availableYield] = await payFiContract.read.getBalanceInfo([
user.account.address,
]);
assert.equal(principal, depositAmount, "本金不應改變");
// 預設 APY 為 5% (500),10,000 本金 1 年預期利息約為 500 USDC
const expectedYield = parseEther("500");
const difference = availableYield > expectedYield ? availableYield - expectedYield : expectedYield - availableYield;
assert.ok(difference < parseEther("0.1"), `利息計算偏差過大: ${availableYield}`);
});
it("PayFi 核心支付:當利息充足時,應實現「本金不動,利息消費」", async function () {
const { payFiContract, mockUSDC, user, merchant, testClient } = await deployFixture();
const depositAmount = parseEther("10000");
const paymentAmount = parseEther("200");
await payFiContract.write.deposit([depositAmount], { account: user.account });
// 🔥 【關鍵修復】推進時間 1 年
await testClient.increaseTime({ seconds: 31536000 });
await testClient.mine({ blocks: 1 });
// 向商家支付
await payFiContract.write.payMerchant([merchant.account.address, paymentAmount], {
account: user.account,
});
const [principal, availableYield] = await payFiContract.read.getBalanceInfo([
user.account.address,
]);
const merchantBalance = await mockUSDC.read.balanceOf([merchant.account.address]);
assert.equal(merchantBalance, paymentAmount, "商家未收到正確款項");
assert.equal(principal, depositAmount, "本金不應被扣除");
assert.ok(availableYield < parseEther("301") && availableYield > parseEther("299"), "剩餘利息結算不正確");
});
it("異常邊界處理:當利息不足以支付時,應自動扣除本金補足", async function () {
const { payFiContract, mockUSDC, user, merchant, testClient } = await deployFixture();
const depositAmount = parseEther("1000");
await payFiContract.write.deposit([depositAmount], { account: user.account });
// 🔥 【關鍵修復】只過了 10 秒
await testClient.increaseTime({ seconds: 10 });
await testClient.mine({ blocks: 1 });
const paymentAmount = parseEther("100");
await payFiContract.write.payMerchant([merchant.account.address, paymentAmount], {
account: user.account,
});
const [principal, availableYield] = await payFiContract.read.getBalanceInfo([
user.account.address,
]);
const merchantBalance = await mockUSDC.read.balanceOf([merchant.account.address]);
assert.equal(merchantBalance, paymentAmount, "商家應足額收到款項");
assert.ok(principal < parseEther("901") && principal > parseEther("899"), "本金扣除不正確");
assert.equal(availableYield, 0n, "剩餘利息應被歸零");
});
it("贖回機制驗證:用戶隨時可提現全部或部分本金", async function () {
const { payFiContract, user } = await deployFixture();
const depositAmount = parseEther("5000");
await payFiContract.write.deposit([depositAmount], { account: user.account });
const withdrawAmount = parseEther("2000");
await payFiContract.write.withdraw([withdrawAmount], { account: user.account });
const [principal] = await payFiContract.read.getBalanceInfo([user.account.address]);
assert.equal(principal, parseEther("3000"), "提取後留存本金不匹配");
});
it("權限與治理拦截:非合約 Owner 無權調整收益池 APY", async function () {
const { payFiContract, user } = await deployFixture();
const newAPY = 1000n;
await assert.rejects(
async () => {
await payFiContract.write.setAPY([newAPY], {
account: user.account,
});
},
/OwnableUnauthorizedAccount/,
"非管理員應被拒絕修改 APY"
);
});
});
四、部署脚本
// scripts/deploy.js
import { network, artifacts } from "hardhat";
import { parseUnits, parseEther } from "viem";
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 MockStablecoinArtifact = await artifacts.readArtifact("MockStablecoin");
const PayFiYieldPaymentArtifact = await artifacts.readArtifact("PayFiYieldPayment");
// 部署(构造函数参数:recipient, initialOwner)
const MockStablecoinHash = await deployer.deployContract({
abi: MockStablecoinArtifact.abi,//获取abi
bytecode: MockStablecoinArtifact.bytecode,//硬编码
args: [parseEther("1000000")],//
});
const MockStablecoinReceipt = await publicClient.waitForTransactionReceipt({ hash: MockStablecoinHash });
console.log("代币合约地址:", MockStablecoinReceipt.contractAddress);
//
const PayFiYieldPaymentHash = await deployer.deployContract({
abi: PayFiYieldPaymentArtifact.abi,//获取abi
bytecode: PayFiYieldPaymentArtifact.bytecode,//硬编码
args: [MockStablecoinReceipt.contractAddress],//
});
// 等待确认并打印地址
const PayFiYieldPaymentReceipt = await publicClient.waitForTransactionReceipt({ hash: PayFiYieldPaymentHash });
console.log("支付合约地址:", PayFiYieldPaymentReceipt.contractAddress);
}
main().catch(console.error);
五、 OpenZeppelin v5 技术防护点解析
Ownable2Step双重验证
传统 Web3 应用常因移交合约权限时“手滑”填错地址,导致合约永久失控。OpenZeppelin v5 引入的Ownable2Step要求新地址必须主动调用acceptOwnership才能接管合约,这为接入外部 RWA 自动化策略机器人提供了极高的安全保障。SafeERC20阻断假充值与返还阻碍
合约使用safeTransfer和safeTransferFrom。这可以防止某些未严格遵循 ERC20 标准(即在失败时返回false而非revert)的恶意或老旧代币钻空子,确保每一次向商家清算或用户提现都具备原子级别的交易安全性。
结论:PayFi 重塑大众金融的未来
通过上述代码实现可以看出,PayFi 的本质是消除传统信用中介,让代码直接捕捉并释放资产的未来现金流。用户不需要放弃资产的所有权,仅凭借资产随时间产生的货币时间价值(TVM) 即可满足高频的商户结算与消费。随着诸如 Solana 的 Soroban(Stellar)等具备极速、低成本基础设施的公链升级,可编程的 PayFi 生态必将在传统金融与 Web3 之间搭建起一条无缝的跨界桥梁。