Solidity 0.8.27 实战|拆解 Web3 全新 PayFi 支付架构

2 阅读11分钟

前言

随着分布式网络技术的成熟,可编程数字资产的年结算规模已正式迈入数十万亿美元的量级。在这种背景下,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)”,从而测试高精度、动态的时间金融逻辑。

以下是基于 viemnode:testnode: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 技术防护点解析

  1. Ownable2Step 双重验证
    传统 Web3 应用常因移交合约权限时“手滑”填错地址,导致合约永久失控。OpenZeppelin v5 引入的 Ownable2Step 要求新地址必须主动调用 acceptOwnership 才能接管合约,这为接入外部 RWA 自动化策略机器人提供了极高的安全保障。
  2. SafeERC20 阻断假充值与返还阻碍
    合约使用 safeTransfersafeTransferFrom。这可以防止某些未严格遵循 ERC20 标准(即在失败时返回 false 而非 revert)的恶意或老旧代币钻空子,确保每一次向商家清算或用户提现都具备原子级别的交易安全性。

结论:PayFi 重塑大众金融的未来

通过上述代码实现可以看出,PayFi 的本质是消除传统信用中介,让代码直接捕捉并释放资产的未来现金流。用户不需要放弃资产的所有权,仅凭借资产随时间产生的货币时间价值(TVM) 即可满足高频的商户结算与消费。随着诸如 Solana 的 Soroban(Stellar)等具备极速、低成本基础设施的公链升级,可编程的 PayFi 生态必将在传统金融与 Web3 之间搭建起一条无缝的跨界桥梁。